Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pixel: add support for Monochrome types such as the SSD1306 #663

Merged
merged 1 commit into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions pixel/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
ptr := (*byte)(unsafe.Add(img.data, offset))
if c != zeroColor {
*((*byte)(ptr)) |= 1 << uint8(y%8)
} else {
*((*byte)(ptr)) &^= 1 << uint8(y%8)
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
}
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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions pixel/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
35 changes: 33 additions & 2 deletions pixel/pixel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
Loading