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() == 2: // Grayscale2bit. offset := index / 4 // 4 pixels per byte shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0 ptr := (*byte)(unsafe.Add(img.data, offset)) raw := *(*uint8)(unsafe.Pointer(&c)) gray := raw & 0b11 mask := byte(0b11 << shift) *ptr = (*ptr &^ mask) | (gray << shift) 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() == 2: // Grayscale2bit. offset := index / 4 // 4 pixels per byte shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0 ptr := (*byte)(unsafe.Add(img.data, offset)) value := ((*ptr) >> shift) & 0b11 return any(Grayscale2bit(value)).(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) } }