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  }