diff --git a/devices/ssd1306/image1bit/image1bit.go b/devices/ssd1306/image1bit/image1bit.go index baa5b85..bb9c813 100644 --- a/devices/ssd1306/image1bit/image1bit.go +++ b/devices/ssd1306/image1bit/image1bit.go @@ -2,14 +2,15 @@ // Use of this source code is governed under the Apache License, Version 2.0 // that can be found in the LICENSE file. -// Package image1bit implements black and white (1 bit per pixel) 2D graphics -// in the memory format of the ssd1306 controller. +// Package image1bit implements black and white (1 bit per pixel) 2D graphics. // // It is compatible with package image/draw. +// +// VerticalLSB is the only bit packing implemented as it is used by the +// ssd1306. Others would be VerticalMSB, HorizontalLSB and HorizontalMSB. package image1bit import ( - "errors" "image" "image/color" "image/draw" @@ -18,12 +19,16 @@ import ( // Bit implements a 1 bit color. type Bit bool -// RGBA returns either all white or all black and transparent. +// RGBA returns either all white or all black. +// +// Technically the monochrome display could be colored but this information is +// unavailable here. To use a colored display, use the 1 bit image as a mask +// for a color. func (b Bit) RGBA() (uint32, uint32, uint32, uint32) { if b { return 65535, 65535, 65535, 65535 } - return 0, 0, 0, 0 + return 0, 0, 0, 65535 } func (b Bit) String() string { @@ -35,98 +40,133 @@ func (b Bit) String() string { // Possible bitness. const ( - On = Bit(true) - Off = Bit(false) + On Bit = true + Off Bit = false ) -// Image is a 1 bit (black and white) image. +// BitModel is the color Model for 1 bit color. +var BitModel = color.ModelFunc(convert) + +// VerticalLSB is a 1 bit (black and white) image. // -// The packing used is unusual, each byte is 8 vertical pixels, with each byte -// stride being an horizontal band of 8 pixels high. +// Each byte is 8 vertical pixels. Each stride is an horizontal band of 8 +// pixels high with LSB first. So the first byte represent the following +// pixels, with lowest bit being the top left pixel. +// +// 0 x x x x x x x +// 1 x x x x x x x +// 2 x x x x x x x +// 3 x x x x x x x +// 4 x x x x x x x +// 5 x x x x x x x +// 6 x x x x x x x +// 7 x x x x x x x // // It is designed specifically to work with SSD1306 OLED display controler. -type Image struct { - W int - H int - Buf []byte // Can be passed directly to ssd1306.(*Dev).Write() +type VerticalLSB struct { + // Pix holds the image's pixels, as vertically LSB-first packed bitmap. It + // can be passed directly to ssd1306.Dev.Write() + Pix []byte + // Stride is the Pix stride (in bytes) between vertically adjacent 8 pixels + // horizontal bands. + Stride int + // Rect is the image's bounds. + Rect image.Rectangle } -// New returns an initialized Image instance. -func New(r image.Rectangle) (*Image, error) { - h := r.Dy() +// NewVerticalLSB returns an initialized VerticalLSB instance. +func NewVerticalLSB(r image.Rectangle) *VerticalLSB { w := r.Dx() - if h&7 != 0 { - return nil, errors.New("image1bit: height must be multiple of 8") - } - return &Image{w, h, make([]byte, w*h/8)}, nil -} - -// SetAll sets all pixels to On. -func (i *Image) SetAll() { - for j := range i.Buf { - i.Buf[j] = 0xFF - } + // Round down. + minY := r.Min.Y &^ 7 + // Round up. + maxY := (r.Max.Y + 7) & ^7 + bands := (maxY - minY) / 8 + return &VerticalLSB{Pix: make([]byte, w*bands), Stride: w, Rect: r} } -// Clear sets all pixels to Off. -func (i *Image) Clear() { - for j := range i.Buf { - i.Buf[j] = 0 - } +// ColorModel implements image.Image. +func (i *VerticalLSB) ColorModel() color.Model { + return BitModel } -// Inverse changes all On pixels to Off and Off pixels to On. -func (i *Image) Inverse() { - for j := range i.Buf { - i.Buf[j] ^= 0xFF - } +// Bounds implements image.Image. +func (i *VerticalLSB) Bounds() image.Rectangle { + return i.Rect } -// ColorModel implements image.Image. -func (i *Image) ColorModel() color.Model { - return color.ModelFunc(convert) +// At implements image.Image. +func (i *VerticalLSB) At(x, y int) color.Color { + return i.BitAt(x, y) } -// Bounds implements image.Image. -func (i *Image) Bounds() image.Rectangle { - return image.Rectangle{Max: image.Point{X: i.W, Y: i.H}} +// BitAt is the optimized version of At(). +func (i *VerticalLSB) BitAt(x, y int) Bit { + if !(image.Point{x, y}.In(i.Rect)) { + return Off + } + offset, mask := i.PixOffset(x, y) + return Bit(i.Pix[offset]&mask != 0) } -// At implements image.Image. -func (i *Image) At(x, y int) color.Color { - return i.AtBit(x, y) +// Opaque scans the entire image and reports whether it is fully opaque. +func (i *VerticalLSB) Opaque() bool { + return true } -// AtBit is the optimized version of At(). -func (i *Image) AtBit(x, y int) Bit { - offset := x + y/8*i.W - mask := byte(1 << byte(y&7)) - return Bit(i.Buf[offset]&mask != 0) +// PixOffset returns the index of the first element of Pix that corresponds to +// the pixel at (x, y) and the corresponding mask. +func (i *VerticalLSB) PixOffset(x, y int) (int, byte) { + // Adjust band. + minY := i.Rect.Min.Y &^ 7 + pY := (y - minY) + offset := pY/8*i.Stride + (x - i.Rect.Min.X) + bit := uint(pY & 7) + return offset, 1 << bit } // Set implements draw.Image -func (i *Image) Set(x, y int, c color.Color) { +func (i *VerticalLSB) Set(x, y int, c color.Color) { i.SetBit(x, y, convertBit(c)) } // SetBit is the optimized version of Set(). -func (i *Image) SetBit(x, y int, b Bit) { - if x >= 0 && x < i.W { - if y >= 0 && y < i.H { - offset := x + y/8*i.W - mask := byte(1 << byte(y&7)) - if b { - i.Buf[offset] |= mask - } else { - i.Buf[offset] &^= mask - } - } +func (i *VerticalLSB) SetBit(x, y int, b Bit) { + if !(image.Point{x, y}.In(i.Rect)) { + return + } + offset, mask := i.PixOffset(x, y) + if b { + i.Pix[offset] |= mask + } else { + i.Pix[offset] &^= mask + } +} + +/* +// SubImage returns an image representing the portion of the image p visible +// through r. The returned value shares pixels with the original image. +func (i *VerticalLSB) SubImage(r image.Rectangle) image.Image { + r = r.Intersect(i.Rect) + // If r1 and r2 are Rectangles, r1.Intersect(r2) is not guaranteed to be + // inside either r1 or r2 if the intersection is empty. Without explicitly + // checking for this, the Pix[i:] expression below can panic. + if r.Empty() { + return &VerticalLSB{} + } + offset, mask := i.PixOffset(r.Min.X, r.Min.Y) + // TODO(maruel): Adjust with mask. + return &VerticalLSB{ + Pix: i.Pix[offset:], + Stride: i.Stride, + Rect: r, } } +*/ // -var _ draw.Image = &Image{} +var _ draw.Image = &VerticalLSB{} // Anything not transparent and not pure black is white. func convert(c color.Color) color.Color { diff --git a/devices/ssd1306/image1bit/image1bit_test.go b/devices/ssd1306/image1bit/image1bit_test.go index af9cdd7..d8c851d 100644 --- a/devices/ssd1306/image1bit/image1bit_test.go +++ b/devices/ssd1306/image1bit/image1bit_test.go @@ -5,7 +5,6 @@ package image1bit import ( - "bytes" "image" "image/color" "testing" @@ -13,10 +12,10 @@ import ( func TestBit(t *testing.T) { if r, g, b, a := On.RGBA(); r != 65535 || g != r || b != r || a != r { - t.Fail() + t.Fatal(r, g, b, a) } - if r, g, b, a := Off.RGBA(); r != 0 || g != r || b != r || a != r { - t.Fail() + if r, g, b, a := Off.RGBA(); r != 0 || g != r || b != r || a != 65535 { + t.Fatal(r, g, b, a) } if On.String() != "On" || Off.String() != "Off" { t.Fail() @@ -25,44 +24,151 @@ func TestBit(t *testing.T) { t.Fail() } } -func TestImageNew(t *testing.T) { - if img, err := New(image.Rect(0, 0, 8, 7)); img != nil || err == nil { - t.Fail() + +func TestVerticalLSB_NewVerticalLSB(t *testing.T) { + data := []struct { + r image.Rectangle + l int + stride int + }{ + // Empty. + { + image.Rect(0, 0, 0, 1), + 0, + 0, + }, + // Empty. + { + image.Rect(0, 0, 1, 0), + 0, + 1, + }, + // 1 horizontal band of 1px high, 1px wide. + { + image.Rect(0, 0, 1, 1), + 1, + 1, + }, + { + image.Rect(0, 1, 1, 2), + 1, + 1, + }, + // 1 horizontal band of 8px high, 1px wide. + { + image.Rect(0, 0, 1, 8), + 1, + 1, + }, + // 1 horizontal band of 1px high, 9px wide. + { + image.Rect(0, 0, 9, 1), + 9, + 9, + }, + // 2 horizontal bands of 1px high, 1px wide. + { + image.Rect(0, 0, 1, 9), + 2, + 1, + }, + // 2 horizontal bands, 1px wide. + { + image.Rect(0, 1, 1, 9), + 2, + 1, + }, + // 2 horizontal bands, 1px wide. + { + image.Rect(0, 7, 1, 9), + 2, + 1, + }, + // 2 horizontal bands, 1px wide. + { + image.Rect(0, 7, 1, 16), + 2, + 1, + }, + // 3 horizontal bands, 1px wide. + { + image.Rect(0, 7, 1, 17), + 3, + 1, + }, + // 3 horizontal bands, 1px wide. + { + image.Rect(0, 7, 1, 17), + 3, + 1, + }, + // 3 horizontal bands, 9px wide. + { + image.Rect(0, 7, 9, 17), + 3 * 9, + 9, + }, + // Negative X. + { + image.Rect(-1, 0, 0, 1), + 1, + 1, + }, + // Negative Y. + { + image.Rect(0, -1, 1, 0), + 1, + 1, + }, + { + image.Rect(0, -1, 1, 1), + 2, + 1, + }, } - if img, err := New(image.Rect(0, 0, 1, 8)); img == nil || err != nil { - t.Fail() + for i, line := range data { + img := NewVerticalLSB(line.r) + if r := img.Bounds(); r != line.r { + t.Fatalf("#%d: expected %v; actual %v", i, line.r, r) + } + if l := len(img.Pix); l != line.l { + t.Fatalf("#%d: len(img.Pix) expected %v; actual %v for %v", i, line.l, l, line.r) + } + if img.Stride != line.stride { + t.Fatalf("#%d: img.Stride expected %v; actual %v for %v", i, line.stride, img.Stride, line.r) + } } } -func TestImagePixels(t *testing.T) { - img, _ := New(image.Rect(0, 0, 1, 8)) - if !bytes.Equal(img.Buf, []byte{0x00}) { - t.Fatal("starts black") - } - img.SetAll() - if !bytes.Equal(img.Buf, []byte{0xFF}) { - t.Fatal("SetAll sets white") - } - img.Clear() - if !bytes.Equal(img.Buf, []byte{0x00}) { - t.Fatal("Clear sets black") - } - img.Set(0, 2, color.NRGBA{0x80, 0x80, 0x80, 0xFF}) - img.Set(1, 2, color.NRGBA{0x80, 0x80, 0x80, 0xFF}) - img.Inverse() - if !bytes.Equal(img.Buf, []byte{0xFB}) { - t.Fatalf("inverse %# v", img.Buf) - } - if img.At(0, 2).(Bit) != Off { - t.Fail() +func TestVerticalLSB_At(t *testing.T) { + img := NewVerticalLSB(image.Rect(0, 0, 1, 1)) + img.SetBit(0, 0, On) + c := img.At(0, 0) + if b, ok := c.(Bit); !ok || b != On { + t.Fatal(c, b) } - if r := img.Bounds(); r.Min.X != 0 || r.Min.Y != 0 || r.Max.X != 1 || r.Max.Y != 8 { - t.Fail() + c = img.At(0, 1) + if b, ok := c.(Bit); !ok || b != Off { + t.Fatal(c, b) + } +} + +func TestVerticalLSB_BitAt(t *testing.T) { + img := NewVerticalLSB(image.Rect(0, 0, 1, 1)) + img.SetBit(0, 0, On) + if b := img.BitAt(0, 0); b != On { + t.Fatal(b) + } + if b := img.BitAt(0, 1); b != Off { + t.Fatal(b) } } -func TestColorModel(t *testing.T) { - img, _ := New(image.Rect(0, 0, 1, 8)) +func TestVerticalLSB_ColorModel(t *testing.T) { + img := NewVerticalLSB(image.Rect(0, 0, 1, 8)) + if v := img.ColorModel(); v != BitModel { + t.Fatalf("%s", v) + } if v := img.ColorModel().Convert(color.NRGBA{0x80, 0x80, 0x80, 0xFF}).(Bit); v != On { t.Fatalf("%s", v) } @@ -70,3 +176,90 @@ func TestColorModel(t *testing.T) { t.Fatalf("%s", v) } } + +func TestVerticalLSB_Opaque(t *testing.T) { + if !NewVerticalLSB(image.Rect(0, 0, 1, 8)).Opaque() { + t.Fatal("image is always opaque") + } +} + +func TestVerticalLSB_PixOffset(t *testing.T) { + data := []struct { + r image.Rectangle + x, y int + offset int + mask byte + }{ + { + image.Rect(0, 0, 1, 1), + 0, 0, + 0, 0x01, + }, + { + image.Rect(0, 0, 1, 8), + 0, 1, + 0, 0x02, + }, + { + image.Rect(0, 0, 3, 16), + 1, 5, + 1, 0x20, + }, + { + image.Rect(-1, -1, 3, 16), + 1, 5, + 6, 0x20, + }, + } + for i, line := range data { + img := NewVerticalLSB(line.r) + offset, mask := img.PixOffset(line.x, line.y) + if offset != line.offset || mask != line.mask { + t.Fatalf("#%d: expected offset:%v, mask:0x%02X; actual offset:%v, mask:0x%02X", i, line.offset, line.mask, offset, mask) + } + } +} + +func TestVerticalLSB_SetBit1x1(t *testing.T) { + img := NewVerticalLSB(image.Rect(0, 0, 1, 1)) + if img.Pix[0] != 0 { + t.Fatal(img.Pix) + } + if img.SetBit(0, 1, On); img.Pix[0] != 0 { + t.Fatal(img.Pix) + } + if img.SetBit(0, 0, On); img.Pix[0] != 1 { + t.Fatal(img.Pix) + } + if img.SetBit(0, 0, Off); img.Pix[0] != 0 { + t.Fatal(img.Pix) + } +} + +func TestVerticalLSB_SetBit1x8(t *testing.T) { + img := NewVerticalLSB(image.Rect(0, 0, 1, 8)) + if img.Pix[0] != 0 { + t.Fatal(img.Pix) + } + if img.SetBit(0, 7, On); img.Pix[0] != 0x80 { + t.Fatal(img.Pix) + } + if img.SetBit(0, 0, On); img.Pix[0] != 0x81 { + t.Fatal(img.Pix) + } + if img.SetBit(0, 7, Off); img.Pix[0] != 1 { + t.Fatal(img.Pix) + } +} + +func TestVerticalLSB_Set(t *testing.T) { + img := NewVerticalLSB(image.Rect(0, 0, 1, 8)) + img.Set(0, 0, color.NRGBA{0x80, 0x80, 0x80, 0xFF}) + img.Set(0, 1, color.NRGBA{0x7F, 0x80, 0x80, 0xFF}) + img.Set(0, 2, color.NRGBA{0x7F, 0x7F, 0x80, 0xFF}) + img.Set(0, 3, color.NRGBA{0x7F, 0x7F, 0x7F, 0xFF}) + img.Set(0, 4, color.NRGBA{0x80, 0x80, 0x80, 0x7F}) + if img.Pix[0] != 7 { + t.Fatal(img.Pix) + } +} diff --git a/devices/ssd1306/ssd1306.go b/devices/ssd1306/ssd1306.go index 8cf7044..cdc6a89 100644 --- a/devices/ssd1306/ssd1306.go +++ b/devices/ssd1306/ssd1306.go @@ -198,10 +198,10 @@ func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) { deltaY := r.Min.Y - srcR.Min.Y var pixels []byte - if img, ok := src.(*image1bit.Image); ok { + if img, ok := src.(*image1bit.VerticalLSB); ok { if srcR.Min.X == 0 && srcR.Dx() == d.W && srcR.Min.Y == 0 && srcR.Dy() == d.H { // Fast path. - pixels = img.Buf + pixels = img.Pix } } if pixels == nil { diff --git a/devices/ssd1306/ssd1306_test.go b/devices/ssd1306/ssd1306_test.go index c690bc0..bad9f47 100644 --- a/devices/ssd1306/ssd1306_test.go +++ b/devices/ssd1306/ssd1306_test.go @@ -59,10 +59,7 @@ func TestDraw1D(t *testing.T) { } bounds := dev.Bounds() gray := makeGrayCheckboard(bounds) - img, err := image1bit.New(bounds) - if err != nil { - t.Fatal(err) - } + img := image1bit.NewVerticalLSB(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { img.Set(x, y, gray.At(x, y)) @@ -89,10 +86,7 @@ func Example() { // Draw on it. f := basicfont.Face7x13 - img, err := image1bit.New(dev.Bounds()) - if err != nil { - log.Fatal(err) - } + img := image1bit.NewVerticalLSB(dev.Bounds()) drawer := font.Drawer{ Dst: img, Src: &image.Uniform{image1bit.On},