Skip to content

Commit

Permalink
pixel: add support for MonochromeVertical types such as the SSD1306 d…
Browse files Browse the repository at this point in the history
…isplay has

Signed-off-by: deadprogram <[email protected]>
  • Loading branch information
deadprogram committed Apr 7, 2024
1 parent a74770b commit 5d50f98
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 10 deletions.
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
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))
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 MonochromeVertical
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 TestImageMonochromeVertical(t *testing.T) {
image := pixel.NewImage[pixel.MonochromeVertical](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.MonochromeVertical](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 | MonochromeVertical

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 MonochromeVertical:
return any(NewMonochromeVertical(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 MonochromeVertical bool

func NewMonochromeVertical(r, g, b uint8) MonochromeVertical {
// 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 MonochromeVertical(true)
}
// dark, convert to black
return MonochromeVertical(false)
}

func (c MonochromeVertical) BitsPerPixel() int {
return 1
}

func (c MonochromeVertical) 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

0 comments on commit 5d50f98

Please sign in to comment.