mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
253f8e3220
* pixel: add GrayScale2bit color * pixel: fix spelling of 'GrayScale' to 'Grayscale'
322 lines
9.4 KiB
Go
322 lines
9.4 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() == 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)
|
|
}
|
|
}
|