diff --git a/pixel/image.go b/pixel/image.go index 54633e3..0a3f727 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -22,12 +22,13 @@ func NewImage[T Color](width, height int) Image[T] { } var zeroColor T var data unsafe.Pointer - if zeroColor.BitsPerPixel()%8 == 0 { + 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]) - } else { + 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() @@ -80,10 +81,11 @@ func (img Image[T]) Len() int { func (img Image[T]) RawBuffer() []uint8 { var zeroColor T var numBytes int - if zeroColor.BitsPerPixel()%8 == 0 { + 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) - } else { + 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) @@ -99,7 +101,20 @@ func (img Image[T]) Size() (int, int) { func (img Image[T]) setPixel(index int, c T) { var zeroColor T - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + x := int16(index) % img.width + y := int16(index) / img.width + offset := x + (y/8)*img.width + ptr := (*byte)(unsafe.Add(img.data, offset)) + if c != zeroColor { + *((*byte)(ptr)) |= 1 << uint8(y%8) + } else { + *((*byte)(ptr)) &^= 1 << uint8(y%8) + } + 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)) @@ -147,7 +162,15 @@ func (img Image[T]) Get(x, y int) T { var zeroColor T index := y*int(img.width) + x // index into img.data - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + var c Monochrome + offset := x + (y/8)*int(img.width) + ptr := (*byte)(unsafe.Add(img.data, offset)) + c = (*ptr >> uint8(y%8) & 0x1) == 1 + 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) @@ -181,8 +204,26 @@ func (img Image[T]) Get(x, y int) T { func (img Image[T]) FillSolidColor(color T) { var zeroColor T - // Fast pass for colors of 8, 16, 24, etc bytes in size. - if zeroColor.BitsPerPixel()%8 == 0 { + 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. diff --git a/pixel/image_test.go b/pixel/image_test.go index 42f292c..e7f165e 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -62,3 +62,35 @@ func TestImageRGB444BE(t *testing.T) { } } } + +func TestImageMonochrome(t *testing.T) { + image := pixel.NewImage[pixel.Monochrome](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, expected := range []color.RGBA{ + {R: 0xff, G: 0xff, B: 0xff}, + {G: 0xff}, + {R: 0xff, G: 0xff}, + {G: 0xff, B: 0xff}, + {R: 0x00}, + {G: 0x00, A: 0xff}, + {B: 0x00, A: 0xff}, + } { + encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B) + image.Set(4, 2, encoded) + actual := image.Get(4, 2).RGBA() + switch { + case expected.R == 0 && expected.G == 0 && expected.B == 0: + // should be false eg black + if actual.R != 0 || actual.G != 0 || actual.B != 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + case int(expected.R)+int(expected.G)+int(expected.B) > 128*3: + // should be true eg white + if actual.R == 0 || actual.G == 0 || actual.B == 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + } + } +} diff --git a/pixel/pixel.go b/pixel/pixel.go index 17db4f3..ed91a60 100644 --- a/pixel/pixel.go +++ b/pixel/pixel.go @@ -14,9 +14,9 @@ import ( // Pixel with a particular color, matching the underlying hardware of a // particular display. Each pixel is at least 1 byte in size. -// The color format is sRGB (or close to it) in all cases. +// The color format is sRGB (or close to it) in all cases except for 1-bit. type Color interface { - RGB888 | RGB565BE | RGB555 | RGB444BE + RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome BaseColor } @@ -50,6 +50,8 @@ func NewColor[T Color](r, g, b uint8) T { return any(NewRGB555(r, g, b)).(T) case RGB444BE: return any(NewRGB444BE(r, g, b)).(T) + case Monochrome: + return any(NewMonochrome(r, g, b)).(T) default: panic("unknown color format") } @@ -202,6 +204,35 @@ func (c RGB444BE) RGBA() color.RGBA { return color } +type Monochrome bool + +func NewMonochrome(r, g, b uint8) Monochrome { + // Very simple black/white split. + // This isn't very accurate (especially for sRGB colors) but is close enough. + if int(r)+int(g)+int(b) > 128*3 { // light, convert to white + return Monochrome(true) + } + // dark, convert to black + return Monochrome(false) +} + +func (c Monochrome) BitsPerPixel() int { + return 1 +} + +func (c Monochrome) RGBA() color.RGBA { + value := uint8(0) + if c { + value = 255 + } + return color.RGBA{ + R: value, + G: value, + B: value, + A: 255, + } +} + // Gamma brightness lookup table: // https://victornpb.github.io/gamma-table-generator // gamma = 0.45 steps = 256 range = 0-255