tinygo.org/x/drivers@v0.27.1-0.20240509133757-7dbca2a54349/pixel/image.go (about) 1 package pixel 2 3 import ( 4 "unsafe" 5 ) 6 7 // Image buffer, used for working with the native image format of various 8 // displays. It works a lot like a slice: it can be rescaled while reusing the 9 // underlying buffer and should be passed around by value. 10 type Image[T Color] struct { 11 width int16 12 height int16 13 data unsafe.Pointer 14 } 15 16 // NewImage creates a new image of the given size. 17 func NewImage[T Color](width, height int) Image[T] { 18 if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { 19 // The width/height are stored as 16-bit integers and should never be 20 // negative. 21 panic("NewImage: width/height out of bounds") 22 } 23 var zeroColor T 24 var data unsafe.Pointer 25 switch { 26 case zeroColor.BitsPerPixel()%8 == 0: 27 // Typical formats like RGB888 and RGB565. 28 // Each color starts at a whole byte offset from the start. 29 buf := make([]T, width*height) 30 data = unsafe.Pointer(&buf[0]) 31 default: 32 // Formats like RGB444 that have 12 bits per pixel. 33 // We access these as bytes, so allocate the buffer as a byte slice. 34 bufBits := width * height * zeroColor.BitsPerPixel() 35 bufBytes := (bufBits + 7) / 8 36 buf := make([]byte, bufBytes) 37 data = unsafe.Pointer(&buf[0]) 38 } 39 return Image[T]{ 40 width: int16(width), 41 height: int16(height), 42 data: data, 43 } 44 } 45 46 // Rescale returns a new Image buffer based on the img buffer. 47 // The contents is undefined after the Rescale operation, and any modification 48 // to the returned image will overwrite the underlying image buffer in undefined 49 // ways. It will panic if width*height is larger than img.Len(). 50 func (img Image[T]) Rescale(width, height int) Image[T] { 51 if width*height > img.Len() { 52 panic("Image.Rescale size out of bounds") 53 } 54 return Image[T]{ 55 width: int16(width), 56 height: int16(height), 57 data: img.data, 58 } 59 } 60 61 // LimitHeight returns a subimage with the bottom part cut off, as specified by 62 // height. 63 func (img Image[T]) LimitHeight(height int) Image[T] { 64 if height < 0 || height > int(img.height) { 65 panic("Image.LimitHeight: out of bounds") 66 } 67 return Image[T]{ 68 width: img.width, 69 height: int16(height), 70 data: img.data, 71 } 72 } 73 74 // Len returns the number of pixels in this image buffer. 75 func (img Image[T]) Len() int { 76 return int(img.width) * int(img.height) 77 } 78 79 // RawBuffer returns a byte slice that can be written directly to the screen 80 // using DrawRGBBitmap8. 81 func (img Image[T]) RawBuffer() []uint8 { 82 var zeroColor T 83 var numBytes int 84 switch { 85 case zeroColor.BitsPerPixel()%8 == 0: 86 // Each color starts at a whole byte offset. 87 numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height) 88 default: 89 // Formats like RGB444 that aren't a whole number of bytes. 90 numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height) 91 numBytes = (numBits + 7) / 8 // round up (see NewImage) 92 } 93 return unsafe.Slice((*byte)(img.data), numBytes) 94 } 95 96 // Size returns the image size. 97 func (img Image[T]) Size() (int, int) { 98 return int(img.width), int(img.height) 99 } 100 101 func (img Image[T]) setPixel(index int, c T) { 102 var zeroColor T 103 104 switch { 105 case zeroColor.BitsPerPixel() == 1: 106 // Monochrome. 107 x := int16(index) % img.width 108 y := int16(index) / img.width 109 offset := x + (y/8)*img.width 110 ptr := (*byte)(unsafe.Add(img.data, offset)) 111 if c != zeroColor { 112 *((*byte)(ptr)) |= 1 << uint8(y%8) 113 } else { 114 *((*byte)(ptr)) &^= 1 << uint8(y%8) 115 } 116 return 117 case zeroColor.BitsPerPixel()%8 == 0: 118 // Each color starts at a whole byte offset. 119 // This is the easy case. 120 offset := index * int(unsafe.Sizeof(zeroColor)) 121 ptr := unsafe.Add(img.data, offset) 122 *((*T)(ptr)) = c 123 return 124 } 125 126 if c, ok := any(c).(RGB444BE); ok { 127 // Special case for RGB444. 128 bitIndex := index * zeroColor.BitsPerPixel() 129 if bitIndex%8 == 0 { 130 byteOffset := bitIndex / 8 131 ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) 132 ptr[0] = uint8(c >> 4) 133 ptr[1] = ptr[1]&0x0f | uint8(c)<<4 // change top bits 134 } else { 135 byteOffset := bitIndex / 8 136 ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) 137 ptr[0] = ptr[0]&0xf0 | uint8(c>>8) // change bottom bits 138 ptr[1] = uint8(c) 139 } 140 return 141 } 142 143 // TODO: the code for RGB444 should be generalized to support any bit size. 144 panic("todo: setPixel for odd bits per pixel") 145 } 146 147 // Set sets the pixel at x, y to the given color. 148 // Use FillSolidColor to efficiently fill the entire image buffer. 149 func (img Image[T]) Set(x, y int, c T) { 150 if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) { 151 panic("Image.Set: out of bounds") 152 } 153 index := y*int(img.width) + x 154 img.setPixel(index, c) 155 } 156 157 // Get returns the color at the given index. 158 func (img Image[T]) Get(x, y int) T { 159 if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) { 160 panic("Image.Get: out of bounds") 161 } 162 var zeroColor T 163 index := y*int(img.width) + x // index into img.data 164 165 switch { 166 case zeroColor.BitsPerPixel() == 1: 167 // Monochrome. 168 var c Monochrome 169 offset := x + (y/8)*int(img.width) 170 ptr := (*byte)(unsafe.Add(img.data, offset)) 171 c = (*ptr >> uint8(y%8) & 0x1) == 1 172 return any(c).(T) 173 case zeroColor.BitsPerPixel()%8 == 0: 174 // Colors like RGB565, RGB888, etc. 175 offset := index * int(unsafe.Sizeof(zeroColor)) 176 ptr := unsafe.Add(img.data, offset) 177 return *((*T)(ptr)) 178 } 179 180 if _, ok := any(zeroColor).(RGB444BE); ok { 181 // Special case for RGB444 that isn't stored in a neat byte multiple. 182 bitIndex := index * zeroColor.BitsPerPixel() 183 var c RGB444BE 184 if bitIndex%8 == 0 { 185 byteOffset := bitIndex / 8 186 ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) 187 c |= RGB444BE(ptr[0]) << 4 188 c |= RGB444BE(ptr[1] >> 4) // load top bits 189 } else { 190 byteOffset := bitIndex / 8 191 ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) 192 c |= RGB444BE(ptr[0]&0x0f) << 8 // load bottom bits 193 c |= RGB444BE(ptr[1]) 194 } 195 return any(c).(T) 196 } 197 198 // TODO: generalize the above code. 199 panic("todo: Image.Get for odd bits per pixel") 200 } 201 202 // FillSolidColor fills the entire image with the given color. 203 // This may be faster than setting individual pixels. 204 func (img Image[T]) FillSolidColor(color T) { 205 var zeroColor T 206 207 switch { 208 case zeroColor.BitsPerPixel() == 1: 209 // Monochrome. 210 var colorByte uint8 211 if color != zeroColor { 212 colorByte = 0xff 213 } 214 numBytes := int(img.width) * int(img.height) / 8 215 for i := 0; i < numBytes; i++ { 216 // TODO: this can be optimized a lot. 217 // - The store can be done as a 32-bit integer, after checking for 218 // alignment. 219 // - Perhaps the loop can be unrolled to improve copy performance. 220 ptr := (*byte)(unsafe.Add(img.data, i)) 221 *((*byte)(ptr)) = colorByte 222 } 223 return 224 225 case zeroColor.BitsPerPixel()%8 == 0: 226 // Fast pass for colors of 8, 16, 24, etc bytes in size. 227 ptr := img.data 228 for i := 0; i < img.Len(); i++ { 229 // TODO: this can be optimized a lot. 230 // - The store can be done as a 32-bit integer, after checking for 231 // alignment. 232 // - Perhaps the loop can be unrolled to improve copy performance. 233 *(*T)(ptr) = color 234 ptr = unsafe.Add(ptr, unsafe.Sizeof(zeroColor)) 235 } 236 return 237 } 238 239 // Special case for RGB444. 240 if c, ok := any(color).(RGB444BE); ok { 241 // RGB444 can be stored in a more optimized way, by storing two colors 242 // at a time instead of setting each color individually. This avoids 243 // loading and masking the old color bits for the half-bytes. 244 var buf [3]uint8 245 buf[0] = uint8(c >> 4) 246 buf[1] = uint8(c)<<4 | uint8(c>>8) 247 buf[2] = uint8(c) 248 rawBuf := unsafe.Slice((*[3]byte)(img.data), img.Len()/2) 249 for i := 0; i < len(rawBuf); i++ { 250 rawBuf[i] = buf 251 } 252 if img.Len()%2 != 0 { 253 // The image contains an uneven number of pixels. 254 // This is uncommon, but it can happen and we have to handle it. 255 img.setPixel(img.Len()-1, color) 256 } 257 return 258 } 259 260 // Fallback for other color formats. 261 for i := 0; i < img.Len(); i++ { 262 img.setPixel(i, color) 263 } 264 }