mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
7d7efe25e7
Set/setPixel and Get weren't using the same indices. I've taken the ones used in Get and applied them to setPixel too. This fixes testImageNoise for the monochrome image.
301 lines
8.8 KiB
Go
301 lines
8.8 KiB
Go
package pixel
|
|
|
|
import (
|
|
"unsafe"
|
|
)
|
|
|
|
// Image buffer, used for working with the native image format of various
|
|
// displays. It works a lot like a slice: it can be rescaled while reusing the
|
|
// underlying buffer and should be passed around by value.
|
|
type Image[T Color] struct {
|
|
width int16
|
|
height int16
|
|
data unsafe.Pointer
|
|
}
|
|
|
|
// NewImage creates a new image of the given size.
|
|
func NewImage[T Color](width, height int) Image[T] {
|
|
if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height {
|
|
// The width/height are stored as 16-bit integers and should never be
|
|
// negative.
|
|
panic("NewImage: width/height out of bounds")
|
|
}
|
|
var zeroColor T
|
|
var data unsafe.Pointer
|
|
switch {
|
|
case zeroColor.BitsPerPixel()%8 == 0:
|
|
// Typical formats like RGB888 and RGB565.
|
|
// Each color starts at a whole byte offset from the start.
|
|
buf := make([]T, width*height)
|
|
data = unsafe.Pointer(&buf[0])
|
|
default:
|
|
// Formats like RGB444 that have 12 bits per pixel.
|
|
// We access these as bytes, so allocate the buffer as a byte slice.
|
|
bufBits := width * height * zeroColor.BitsPerPixel()
|
|
bufBytes := (bufBits + 7) / 8
|
|
buf := make([]byte, bufBytes)
|
|
data = unsafe.Pointer(&buf[0])
|
|
}
|
|
return Image[T]{
|
|
width: int16(width),
|
|
height: int16(height),
|
|
data: data,
|
|
}
|
|
}
|
|
|
|
// NewImageFromBytes creates a new image of the given size using an existing data slice of bytes.
|
|
func NewImageFromBytes[T Color](width, height int, buf []byte) Image[T] {
|
|
if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height {
|
|
// The width/height are stored as 16-bit integers and should never be
|
|
// negative.
|
|
panic("NewImageFromBytes: width/height out of bounds")
|
|
}
|
|
var zeroColor T
|
|
var data unsafe.Pointer
|
|
switch {
|
|
case zeroColor.BitsPerPixel()%8 == 0:
|
|
// Typical formats like RGB888 and RGB565.
|
|
// Each color starts at a whole byte offset from the start.
|
|
if len(buf) != width*height*int(unsafe.Sizeof(zeroColor)) {
|
|
panic("NewImageFromBytes: data slice size mismatch")
|
|
}
|
|
data = unsafe.Pointer(&buf[0])
|
|
default:
|
|
// Formats like RGB444 that have 12 bits per pixel.
|
|
// We access these as bytes, so allocate the buffer as a byte slice.
|
|
bufBits := width * height * zeroColor.BitsPerPixel()
|
|
bufBytes := (bufBits + 7) / 8
|
|
if len(buf) != bufBytes {
|
|
panic("NewImageFromBytes: data slice size mismatch")
|
|
}
|
|
data = unsafe.Pointer(&buf[0])
|
|
}
|
|
return Image[T]{
|
|
width: int16(width),
|
|
height: int16(height),
|
|
data: data,
|
|
}
|
|
}
|
|
|
|
// Rescale returns a new Image buffer based on the img buffer.
|
|
// The contents is undefined after the Rescale operation, and any modification
|
|
// to the returned image will overwrite the underlying image buffer in undefined
|
|
// ways. It will panic if width*height is larger than img.Len().
|
|
func (img Image[T]) Rescale(width, height int) Image[T] {
|
|
if width*height > img.Len() {
|
|
panic("Image.Rescale size out of bounds")
|
|
}
|
|
return Image[T]{
|
|
width: int16(width),
|
|
height: int16(height),
|
|
data: img.data,
|
|
}
|
|
}
|
|
|
|
// LimitHeight returns a subimage with the bottom part cut off, as specified by
|
|
// height.
|
|
func (img Image[T]) LimitHeight(height int) Image[T] {
|
|
if height < 0 || height > int(img.height) {
|
|
panic("Image.LimitHeight: out of bounds")
|
|
}
|
|
return Image[T]{
|
|
width: img.width,
|
|
height: int16(height),
|
|
data: img.data,
|
|
}
|
|
}
|
|
|
|
// Len returns the number of pixels in this image buffer.
|
|
func (img Image[T]) Len() int {
|
|
return int(img.width) * int(img.height)
|
|
}
|
|
|
|
// RawBuffer returns a byte slice that can be written directly to the screen
|
|
// using DrawRGBBitmap8.
|
|
func (img Image[T]) RawBuffer() []uint8 {
|
|
var zeroColor T
|
|
var numBytes int
|
|
switch {
|
|
case zeroColor.BitsPerPixel()%8 == 0:
|
|
// Each color starts at a whole byte offset.
|
|
numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height)
|
|
default:
|
|
// Formats like RGB444 that aren't a whole number of bytes.
|
|
numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height)
|
|
numBytes = (numBits + 7) / 8 // round up (see NewImage)
|
|
}
|
|
return unsafe.Slice((*byte)(img.data), numBytes)
|
|
}
|
|
|
|
// Size returns the image size.
|
|
func (img Image[T]) Size() (int, int) {
|
|
return int(img.width), int(img.height)
|
|
}
|
|
|
|
func (img Image[T]) setPixel(index int, c T) {
|
|
var zeroColor T
|
|
|
|
switch {
|
|
case zeroColor.BitsPerPixel() == 1:
|
|
// Monochrome.
|
|
offset := index / 8
|
|
bits := index % 8
|
|
|
|
ptr := (*byte)(unsafe.Add(img.data, offset))
|
|
if c != zeroColor {
|
|
*((*byte)(ptr)) |= (1 << (7 - uint8(bits)))
|
|
} else {
|
|
*((*byte)(ptr)) &^= (1 << (7 - uint8(bits)))
|
|
}
|
|
|
|
return
|
|
case zeroColor.BitsPerPixel()%8 == 0:
|
|
// Each color starts at a whole byte offset.
|
|
// This is the easy case.
|
|
offset := index * int(unsafe.Sizeof(zeroColor))
|
|
ptr := unsafe.Add(img.data, offset)
|
|
*((*T)(ptr)) = c
|
|
return
|
|
}
|
|
|
|
if c, ok := any(c).(RGB444BE); ok {
|
|
// Special case for RGB444.
|
|
bitIndex := index * zeroColor.BitsPerPixel()
|
|
if bitIndex%8 == 0 {
|
|
byteOffset := bitIndex / 8
|
|
ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset))
|
|
ptr[0] = uint8(c >> 4)
|
|
ptr[1] = ptr[1]&0x0f | uint8(c)<<4 // change top bits
|
|
} else {
|
|
byteOffset := bitIndex / 8
|
|
ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset))
|
|
ptr[0] = ptr[0]&0xf0 | uint8(c>>8) // change bottom bits
|
|
ptr[1] = uint8(c)
|
|
}
|
|
return
|
|
}
|
|
|
|
// TODO: the code for RGB444 should be generalized to support any bit size.
|
|
panic("todo: setPixel for odd bits per pixel")
|
|
}
|
|
|
|
// Set sets the pixel at x, y to the given color.
|
|
// Use FillSolidColor to efficiently fill the entire image buffer.
|
|
func (img Image[T]) Set(x, y int, c T) {
|
|
if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) {
|
|
panic("Image.Set: out of bounds")
|
|
}
|
|
index := y*int(img.width) + x
|
|
img.setPixel(index, c)
|
|
}
|
|
|
|
// Get returns the color at the given index.
|
|
func (img Image[T]) Get(x, y int) T {
|
|
if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) {
|
|
panic("Image.Get: out of bounds")
|
|
}
|
|
var zeroColor T
|
|
index := y*int(img.width) + x // index into img.data
|
|
|
|
switch {
|
|
case zeroColor.BitsPerPixel() == 1:
|
|
// Monochrome.
|
|
var c Monochrome
|
|
offset := index / 8
|
|
bits := index % 8
|
|
ptr := (*byte)(unsafe.Add(img.data, offset))
|
|
c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0
|
|
return any(c).(T)
|
|
case zeroColor.BitsPerPixel()%8 == 0:
|
|
// Colors like RGB565, RGB888, etc.
|
|
offset := index * int(unsafe.Sizeof(zeroColor))
|
|
ptr := unsafe.Add(img.data, offset)
|
|
return *((*T)(ptr))
|
|
}
|
|
|
|
if _, ok := any(zeroColor).(RGB444BE); ok {
|
|
// Special case for RGB444 that isn't stored in a neat byte multiple.
|
|
bitIndex := index * zeroColor.BitsPerPixel()
|
|
var c RGB444BE
|
|
if bitIndex%8 == 0 {
|
|
byteOffset := bitIndex / 8
|
|
ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset))
|
|
c |= RGB444BE(ptr[0]) << 4
|
|
c |= RGB444BE(ptr[1] >> 4) // load top bits
|
|
} else {
|
|
byteOffset := bitIndex / 8
|
|
ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset))
|
|
c |= RGB444BE(ptr[0]&0x0f) << 8 // load bottom bits
|
|
c |= RGB444BE(ptr[1])
|
|
}
|
|
return any(c).(T)
|
|
}
|
|
|
|
// TODO: generalize the above code.
|
|
panic("todo: Image.Get for odd bits per pixel")
|
|
}
|
|
|
|
// FillSolidColor fills the entire image with the given color.
|
|
// This may be faster than setting individual pixels.
|
|
func (img Image[T]) FillSolidColor(color T) {
|
|
var zeroColor T
|
|
|
|
switch {
|
|
case zeroColor.BitsPerPixel() == 1:
|
|
// Monochrome.
|
|
var colorByte uint8
|
|
if color != zeroColor {
|
|
colorByte = 0xff
|
|
}
|
|
numBytes := int(img.width) * int(img.height) / 8
|
|
for i := 0; i < numBytes; i++ {
|
|
// TODO: this can be optimized a lot.
|
|
// - The store can be done as a 32-bit integer, after checking for
|
|
// alignment.
|
|
// - Perhaps the loop can be unrolled to improve copy performance.
|
|
ptr := (*byte)(unsafe.Add(img.data, i))
|
|
*((*byte)(ptr)) = colorByte
|
|
}
|
|
return
|
|
|
|
case zeroColor.BitsPerPixel()%8 == 0:
|
|
// Fast pass for colors of 8, 16, 24, etc bytes in size.
|
|
ptr := img.data
|
|
for i := 0; i < img.Len(); i++ {
|
|
// TODO: this can be optimized a lot.
|
|
// - The store can be done as a 32-bit integer, after checking for
|
|
// alignment.
|
|
// - Perhaps the loop can be unrolled to improve copy performance.
|
|
*(*T)(ptr) = color
|
|
ptr = unsafe.Add(ptr, unsafe.Sizeof(zeroColor))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Special case for RGB444.
|
|
if c, ok := any(color).(RGB444BE); ok {
|
|
// RGB444 can be stored in a more optimized way, by storing two colors
|
|
// at a time instead of setting each color individually. This avoids
|
|
// loading and masking the old color bits for the half-bytes.
|
|
var buf [3]uint8
|
|
buf[0] = uint8(c >> 4)
|
|
buf[1] = uint8(c)<<4 | uint8(c>>8)
|
|
buf[2] = uint8(c)
|
|
rawBuf := unsafe.Slice((*[3]byte)(img.data), img.Len()/2)
|
|
for i := 0; i < len(rawBuf); i++ {
|
|
rawBuf[i] = buf
|
|
}
|
|
if img.Len()%2 != 0 {
|
|
// The image contains an uneven number of pixels.
|
|
// This is uncommon, but it can happen and we have to handle it.
|
|
img.setPixel(img.Len()-1, color)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fallback for other color formats.
|
|
for i := 0; i < img.Len(); i++ {
|
|
img.setPixel(i, color)
|
|
}
|
|
}
|