image1bit: make it more like stdlib

- Rename Image to VerticalLSB, so that the 3 other encodings (vertical MSB,
  horizontal LSB and MSB) can be added at a later point if ever needed without
  breaking the API.
- Use .Pix, .Stride and .Rect so it is more similar to how stdlib image package
  does it.
- Export the color model properly as BitModel.
- Remove frivolous methods.
pull/1/head
Marc-Antoine Ruel 9 years ago
parent 94686e7f5d
commit a0e9236c75

@ -2,14 +2,15 @@
// Use of this source code is governed under the Apache License, Version 2.0 // Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file. // that can be found in the LICENSE file.
// Package image1bit implements black and white (1 bit per pixel) 2D graphics // Package image1bit implements black and white (1 bit per pixel) 2D graphics.
// in the memory format of the ssd1306 controller.
// //
// It is compatible with package image/draw. // 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 package image1bit
import ( import (
"errors"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
@ -18,12 +19,16 @@ import (
// Bit implements a 1 bit color. // Bit implements a 1 bit color.
type Bit bool 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) { func (b Bit) RGBA() (uint32, uint32, uint32, uint32) {
if b { if b {
return 65535, 65535, 65535, 65535 return 65535, 65535, 65535, 65535
} }
return 0, 0, 0, 0 return 0, 0, 0, 65535
} }
func (b Bit) String() string { func (b Bit) String() string {
@ -35,98 +40,133 @@ func (b Bit) String() string {
// Possible bitness. // Possible bitness.
const ( const (
On = Bit(true) On Bit = true
Off = Bit(false) 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 // Each byte is 8 vertical pixels. Each stride is an horizontal band of 8
// stride being an horizontal band of 8 pixels high. // 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. // It is designed specifically to work with SSD1306 OLED display controler.
type Image struct { type VerticalLSB struct {
W int // Pix holds the image's pixels, as vertically LSB-first packed bitmap. It
H int // can be passed directly to ssd1306.Dev.Write()
Buf []byte // Can be passed directly to ssd1306.(*Dev).Write() Pix []byte
} // Stride is the Pix stride (in bytes) between vertically adjacent 8 pixels
// horizontal bands.
// New returns an initialized Image instance. Stride int
func New(r image.Rectangle) (*Image, error) { // Rect is the image's bounds.
h := r.Dy() Rect image.Rectangle
}
// NewVerticalLSB returns an initialized VerticalLSB instance.
func NewVerticalLSB(r image.Rectangle) *VerticalLSB {
w := r.Dx() w := r.Dx()
if h&7 != 0 { // Round down.
return nil, errors.New("image1bit: height must be multiple of 8") minY := r.Min.Y &^ 7
} // Round up.
return &Image{w, h, make([]byte, w*h/8)}, nil maxY := (r.Max.Y + 7) & ^7
bands := (maxY - minY) / 8
return &VerticalLSB{Pix: make([]byte, w*bands), Stride: w, Rect: r}
} }
// SetAll sets all pixels to On. // ColorModel implements image.Image.
func (i *Image) SetAll() { func (i *VerticalLSB) ColorModel() color.Model {
for j := range i.Buf { return BitModel
i.Buf[j] = 0xFF
}
} }
// Clear sets all pixels to Off. // Bounds implements image.Image.
func (i *Image) Clear() { func (i *VerticalLSB) Bounds() image.Rectangle {
for j := range i.Buf { return i.Rect
i.Buf[j] = 0
}
} }
// Inverse changes all On pixels to Off and Off pixels to On. // At implements image.Image.
func (i *Image) Inverse() { func (i *VerticalLSB) At(x, y int) color.Color {
for j := range i.Buf { return i.BitAt(x, y)
i.Buf[j] ^= 0xFF
}
} }
// ColorModel implements image.Image. // BitAt is the optimized version of At().
func (i *Image) ColorModel() color.Model { func (i *VerticalLSB) BitAt(x, y int) Bit {
return color.ModelFunc(convert) if !(image.Point{x, y}.In(i.Rect)) {
return Off
} }
offset, mask := i.PixOffset(x, y)
// Bounds implements image.Image. return Bit(i.Pix[offset]&mask != 0)
func (i *Image) Bounds() image.Rectangle {
return image.Rectangle{Max: image.Point{X: i.W, Y: i.H}}
} }
// At implements image.Image. // Opaque scans the entire image and reports whether it is fully opaque.
func (i *Image) At(x, y int) color.Color { func (i *VerticalLSB) Opaque() bool {
return i.AtBit(x, y) return true
} }
// AtBit is the optimized version of At(). // PixOffset returns the index of the first element of Pix that corresponds to
func (i *Image) AtBit(x, y int) Bit { // the pixel at (x, y) and the corresponding mask.
offset := x + y/8*i.W func (i *VerticalLSB) PixOffset(x, y int) (int, byte) {
mask := byte(1 << byte(y&7)) // Adjust band.
return Bit(i.Buf[offset]&mask != 0) 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 // 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)) i.SetBit(x, y, convertBit(c))
} }
// SetBit is the optimized version of Set(). // SetBit is the optimized version of Set().
func (i *Image) SetBit(x, y int, b Bit) { func (i *VerticalLSB) SetBit(x, y int, b Bit) {
if x >= 0 && x < i.W { if !(image.Point{x, y}.In(i.Rect)) {
if y >= 0 && y < i.H { return
offset := x + y/8*i.W }
mask := byte(1 << byte(y&7)) offset, mask := i.PixOffset(x, y)
if b { if b {
i.Buf[offset] |= mask i.Pix[offset] |= mask
} else { } else {
i.Buf[offset] &^= mask 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. // Anything not transparent and not pure black is white.
func convert(c color.Color) color.Color { func convert(c color.Color) color.Color {

@ -5,7 +5,6 @@
package image1bit package image1bit
import ( import (
"bytes"
"image" "image"
"image/color" "image/color"
"testing" "testing"
@ -13,10 +12,10 @@ import (
func TestBit(t *testing.T) { func TestBit(t *testing.T) {
if r, g, b, a := On.RGBA(); r != 65535 || g != r || b != r || a != r { 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 { if r, g, b, a := Off.RGBA(); r != 0 || g != r || b != r || a != 65535 {
t.Fail() t.Fatal(r, g, b, a)
} }
if On.String() != "On" || Off.String() != "Off" { if On.String() != "On" || Off.String() != "Off" {
t.Fail() t.Fail()
@ -25,44 +24,151 @@ func TestBit(t *testing.T) {
t.Fail() t.Fail()
} }
} }
func TestImageNew(t *testing.T) {
if img, err := New(image.Rect(0, 0, 8, 7)); img != nil || err == nil { func TestVerticalLSB_NewVerticalLSB(t *testing.T) {
t.Fail() 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,
},
}
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)
} }
if img, err := New(image.Rect(0, 0, 1, 8)); img == nil || err != nil {
t.Fail()
} }
} }
func TestImagePixels(t *testing.T) { func TestVerticalLSB_At(t *testing.T) {
img, _ := New(image.Rect(0, 0, 1, 8)) img := NewVerticalLSB(image.Rect(0, 0, 1, 1))
if !bytes.Equal(img.Buf, []byte{0x00}) { img.SetBit(0, 0, On)
t.Fatal("starts black") c := img.At(0, 0)
} if b, ok := c.(Bit); !ok || b != On {
img.SetAll() t.Fatal(c, b)
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()
} }
if r := img.Bounds(); r.Min.X != 0 || r.Min.Y != 0 || r.Max.X != 1 || r.Max.Y != 8 { c = img.At(0, 1)
t.Fail() if b, ok := c.(Bit); !ok || b != Off {
t.Fatal(c, b)
} }
} }
func TestColorModel(t *testing.T) { func TestVerticalLSB_BitAt(t *testing.T) {
img, _ := New(image.Rect(0, 0, 1, 8)) 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 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 { if v := img.ColorModel().Convert(color.NRGBA{0x80, 0x80, 0x80, 0xFF}).(Bit); v != On {
t.Fatalf("%s", v) t.Fatalf("%s", v)
} }
@ -70,3 +176,90 @@ func TestColorModel(t *testing.T) {
t.Fatalf("%s", v) 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)
}
}

@ -198,10 +198,10 @@ func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
deltaY := r.Min.Y - srcR.Min.Y deltaY := r.Min.Y - srcR.Min.Y
var pixels []byte 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 { if srcR.Min.X == 0 && srcR.Dx() == d.W && srcR.Min.Y == 0 && srcR.Dy() == d.H {
// Fast path. // Fast path.
pixels = img.Buf pixels = img.Pix
} }
} }
if pixels == nil { if pixels == nil {

@ -59,10 +59,7 @@ func TestDraw1D(t *testing.T) {
} }
bounds := dev.Bounds() bounds := dev.Bounds()
gray := makeGrayCheckboard(bounds) gray := makeGrayCheckboard(bounds)
img, err := image1bit.New(bounds) img := image1bit.NewVerticalLSB(bounds)
if err != nil {
t.Fatal(err)
}
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ { for x := bounds.Min.X; x < bounds.Max.X; x++ {
img.Set(x, y, gray.At(x, y)) img.Set(x, y, gray.At(x, y))
@ -89,10 +86,7 @@ func Example() {
// Draw on it. // Draw on it.
f := basicfont.Face7x13 f := basicfont.Face7x13
img, err := image1bit.New(dev.Bounds()) img := image1bit.NewVerticalLSB(dev.Bounds())
if err != nil {
log.Fatal(err)
}
drawer := font.Drawer{ drawer := font.Drawer{
Dst: img, Dst: img,
Src: &image.Uniform{image1bit.On}, Src: &image.Uniform{image1bit.On},

Loading…
Cancel
Save