mirror of https://github.com/periph/devices
epd/image2bit: e-paper 2 bit gray scale bit plane image format (#438)
Add image format with two bit planes as used by some waveshare e-Paper devices.pull/1/head
parent
cadf2cf1f3
commit
8284066f76
@ -0,0 +1,169 @@
|
||||
// Copyright 2020 The Periph Authors. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// Package image2bit implements two bit gray scale (white, light gray,
|
||||
// dark gray, black) 2D graphics.
|
||||
//
|
||||
// It is compatible with package image/draw.
|
||||
//
|
||||
// The bit packing format is the same as used by waveshare e-Paper
|
||||
// displays such as the 4.2 inch display.
|
||||
package image2bit
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
)
|
||||
|
||||
// Gray implements a 2 bit color.
|
||||
type Gray byte
|
||||
|
||||
// RGBA returns either black, dark gray, light gray or white.
|
||||
func (b Gray) RGBA() (uint32, uint32, uint32, uint32) {
|
||||
switch b {
|
||||
case 0:
|
||||
return 0, 0, 0, 65535
|
||||
case 1:
|
||||
return 0x5555, 0x5555, 0x5555, 0xffff
|
||||
case 2:
|
||||
return 0xaaaa, 0xaaaa, 0xaaaa, 0xffff
|
||||
default:
|
||||
return 0xffff, 0xffff, 0xffff, 0xffff
|
||||
}
|
||||
}
|
||||
|
||||
func (b Gray) String() string {
|
||||
switch b {
|
||||
case 0:
|
||||
return "black"
|
||||
case 1:
|
||||
return "dark gray"
|
||||
case 2:
|
||||
return "light gray"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// All possible colors
|
||||
const (
|
||||
White Gray = 3
|
||||
LightGray Gray = 2
|
||||
DarkGray Gray = 1
|
||||
Black Gray = 0
|
||||
)
|
||||
|
||||
// GrayModel is the color Model for 2 bit gray scale.
|
||||
var GrayModel = color.ModelFunc(convert)
|
||||
|
||||
// BitPlane is a 2 bit gray scale image. To match the wire format
|
||||
// for waveshare e-Paper the two bits per pixel is stored across two bitmaps.
|
||||
// PixMSB contains the most significant bit, PixLSB contains the least significant bit.
|
||||
//
|
||||
// White LightGray DarkGray Black
|
||||
// PixMSB 1 1 0 0
|
||||
// PixLSB 1 0 1 0
|
||||
//
|
||||
// The following example shows the stored data for an 8 pixel wide image, 1 pixel high:
|
||||
// PixMSB []byte{0b10100000}
|
||||
// PixLSB []byte{0b10000000}
|
||||
//
|
||||
// It has a black background, the first pixel is white, and the third pixel LightGray.
|
||||
type BitPlane struct {
|
||||
// PixMSB holds the image's most significant bit as a horizontally packed bitmap.
|
||||
PixMSB []byte
|
||||
// PixLSB holds the image's least significant bit as a horizontally packed bitmap.
|
||||
PixLSB []byte
|
||||
// Rect is the image's bounds.
|
||||
Rect image.Rectangle
|
||||
|
||||
// Stride is the number of pixels on each horizontal line, including padding
|
||||
Stride int
|
||||
}
|
||||
|
||||
// NewBitPlane returns an initialized BitPlane instance, all black.
|
||||
func NewBitPlane(r image.Rectangle) *BitPlane {
|
||||
// stride is width rounded up to the next byte
|
||||
stride := ((r.Dx() + 7) &^ 7)
|
||||
|
||||
size := (r.Dy() * stride) / 8
|
||||
return &BitPlane{PixMSB: make([]byte, size), PixLSB: make([]byte, size), Rect: r, Stride: stride}
|
||||
}
|
||||
|
||||
// ColorModel implements image.Image.
|
||||
func (i *BitPlane) ColorModel() color.Model {
|
||||
return GrayModel
|
||||
}
|
||||
|
||||
// Bounds implements image.Image.
|
||||
func (i *BitPlane) Bounds() image.Rectangle {
|
||||
return i.Rect
|
||||
}
|
||||
|
||||
// At implements image.Image.
|
||||
func (i *BitPlane) At(x, y int) color.Color {
|
||||
return i.GrayAt(x, y)
|
||||
}
|
||||
|
||||
// GrayAt is the optimized version of At().
|
||||
func (i *BitPlane) GrayAt(x, y int) Gray {
|
||||
if !(image.Point{x, y}.In(i.Rect)) {
|
||||
return Black
|
||||
}
|
||||
|
||||
byteIndex, bitIndex, _ := i.getOffset(x, y)
|
||||
|
||||
return Gray(((i.PixMSB[byteIndex]>>bitIndex)&1)<<1 | i.PixLSB[byteIndex]>>bitIndex&1)
|
||||
}
|
||||
|
||||
// Opaque scans the entire image and reports whether it is fully opaque.
|
||||
func (i *BitPlane) Opaque() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Set implements draw.Image
|
||||
func (i *BitPlane) Set(x, y int, c color.Color) {
|
||||
i.SetGray(x, y, convertGray(c))
|
||||
}
|
||||
|
||||
// SetGray is the optimized version of Set().
|
||||
func (i *BitPlane) SetGray(x, y int, b Gray) {
|
||||
if !(image.Point{x, y}.In(i.Rect)) {
|
||||
return
|
||||
}
|
||||
|
||||
byteIndex, bitIndex, mask := i.getOffset(x, y)
|
||||
|
||||
i.PixMSB[byteIndex] = byte((i.PixMSB[byteIndex] & mask) | (byte(b>>1) << bitIndex))
|
||||
i.PixLSB[byteIndex] = byte((i.PixLSB[byteIndex] & mask) | (byte(b&1) << bitIndex))
|
||||
}
|
||||
|
||||
func (i *BitPlane) getOffset(x, y int) (byteIndex, bitIndex uint, mask byte) {
|
||||
bitIndex = uint(y*i.Stride + x)
|
||||
byteIndex = bitIndex / 8
|
||||
bitIndex = 7 - (bitIndex % 8)
|
||||
mask = byte(0xff ^ (0x01 << bitIndex))
|
||||
return
|
||||
}
|
||||
|
||||
// convert color to gray as color.Color
|
||||
func convert(c color.Color) color.Color {
|
||||
return convertGray(c)
|
||||
}
|
||||
|
||||
// convert color to gray
|
||||
func convertGray(c color.Color) Gray {
|
||||
switch t := c.(type) {
|
||||
case Gray:
|
||||
return t
|
||||
default:
|
||||
r, g, b, _ := c.RGBA()
|
||||
// TODO something fancy, how to weight R/G/B
|
||||
return Gray((r | g | b) >> 14) // Use two most significant bits.
|
||||
}
|
||||
}
|
||||
|
||||
// verify that we satisfy the draw.Image interface
|
||||
var _ draw.Image = &BitPlane{}
|
||||
@ -0,0 +1,172 @@
|
||||
// Copyright 2020 The Periph Authors. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package image2bit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetOffset(t *testing.T) {
|
||||
tb := NewBitPlane(image.Rect(0, 0, 16, 16))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
x, y int
|
||||
|
||||
byteIndex, bitIndex uint
|
||||
mask byte
|
||||
}{
|
||||
{
|
||||
name: "bit order, first, edge",
|
||||
|
||||
x: 0, y: 0,
|
||||
|
||||
byteIndex: 0, bitIndex: 7, mask: 0x7f, // 0b01111111
|
||||
},
|
||||
{
|
||||
name: "bit order 2",
|
||||
|
||||
x: 1, y: 0,
|
||||
|
||||
byteIndex: 0, bitIndex: 6, mask: 0xbf, // 0b10111111
|
||||
},
|
||||
{
|
||||
name: "bit order, last, edge",
|
||||
|
||||
x: 7, y: 0,
|
||||
|
||||
byteIndex: 0, bitIndex: 0, mask: 0xfe, // 0b11111110
|
||||
},
|
||||
{
|
||||
name: "byte index",
|
||||
|
||||
x: 1 + 8, y: 0,
|
||||
|
||||
byteIndex: 1, bitIndex: 6, mask: 0xbf, // 0b10111111
|
||||
},
|
||||
{
|
||||
name: "byte index + row",
|
||||
x: 1 + 8,
|
||||
y: 1,
|
||||
|
||||
byteIndex: 16/8 + 1, bitIndex: 6, mask: 0xbf, // 0b10111111
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
byteIndex, bitIndex, mask := tb.getOffset(test.x, test.y)
|
||||
if byteIndex != test.byteIndex || bitIndex != test.bitIndex || mask != test.mask {
|
||||
t.Errorf("getOffset(%d,%d) failed: Got (%v, %v, %02x), expected (%v, %v, %02x)",
|
||||
test.x, test.y,
|
||||
byteIndex, bitIndex, mask,
|
||||
test.byteIndex, test.bitIndex, test.mask)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStride(t *testing.T) {
|
||||
var strides = []int{0, 8, 8, 8, 8, 8, 8, 8, 8, 16, 16, 16, 16, 16, 16, 16, 16, 24}
|
||||
for width, stride := range strides {
|
||||
b := NewBitPlane(image.Rect(0, 0, width, 2))
|
||||
if b.Stride != stride {
|
||||
t.Errorf("Unexpected stride %v for width %v, expected %v", b.Stride, width, stride)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllWhite(t *testing.T) {
|
||||
// Default color is black. Test that setting everything to White
|
||||
// touches all bits
|
||||
tb := NewBitPlane(image.Rect(0, 0, 16, 2))
|
||||
for y := 0; y < tb.Rect.Dy(); y++ {
|
||||
for x := 0; x < tb.Rect.Dx(); x++ {
|
||||
tb.Set(x, y, color.White)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal([]byte{0xFF, 0xFF, 0xFF, 0xFF}, tb.PixLSB) || !bytes.Equal([]byte{0xFF, 0xFF, 0xFF, 0xFF}, tb.PixMSB) {
|
||||
t.Errorf("Expected 4x 0xFF in both planes, got %v, %v", tb.PixLSB, tb.PixMSB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaneOrder(t *testing.T) {
|
||||
tb := NewBitPlane(image.Rect(0, 0, 16, 2))
|
||||
for y := 0; y < tb.Rect.Dy(); y++ {
|
||||
for x := 0; x < tb.Rect.Dx(); x++ {
|
||||
tb.Set(x, y, DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
// The most significant plane should be black for *dark* gray
|
||||
if !bytes.Equal([]byte{0x00, 0x00, 0x00, 0x00}, tb.PixMSB) || !bytes.Equal([]byte{0xFF, 0xFF, 0xFF, 0xFF}, tb.PixLSB) {
|
||||
t.Errorf("Expected 4x 00 in MSB plane, 4x FF in LSB plane, got %v, %v", tb.PixMSB, tb.PixLSB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitPlaneEncoding(t *testing.T) {
|
||||
tb := NewBitPlane(image.Rect(0, 0, 8, 1))
|
||||
|
||||
// "golden image" test for a black image with two pixels set
|
||||
tb.Set(0, 0, White)
|
||||
tb.Set(2, 0, LightGray)
|
||||
|
||||
expectedMSB := []byte{0xa0} // 0b10100000
|
||||
expectedLSB := []byte{0x80} // 0b10000000
|
||||
|
||||
if !bytes.Equal(tb.PixMSB, expectedMSB) || !bytes.Equal(tb.PixLSB, expectedLSB) {
|
||||
t.Errorf("Golden image test failed, got %02x %02x, expected %02x %02x", tb.PixMSB, tb.PixLSB, expectedMSB, expectedLSB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutOfBoundsRead(t *testing.T) {
|
||||
tb := NewBitPlane(image.Rect(0, 0, 32, 32))
|
||||
|
||||
if tb.At(10000, 10000) != Black {
|
||||
t.Error("Expected out of bounds read to return black")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutOfBoundsWrite(t *testing.T) {
|
||||
tb := NewBitPlane(image.Rect(0, 0, 32, 32))
|
||||
|
||||
// will panic if bounds checking is not implemented :)
|
||||
tb.Set(10000, 10000, White)
|
||||
}
|
||||
|
||||
func TestGrayAt(t *testing.T) {
|
||||
tb := NewBitPlane(image.Rect(0, 0, 16, 2))
|
||||
var grays []Gray
|
||||
for y := 0; y < tb.Rect.Dy(); y++ {
|
||||
for x := 0; x < tb.Rect.Dx(); x++ {
|
||||
g := Gray((x ^ y) & 3)
|
||||
tb.Set(x, y, g)
|
||||
grays = append(grays, g)
|
||||
}
|
||||
}
|
||||
|
||||
for y := 0; y < tb.Rect.Dy(); y++ {
|
||||
for x := 0; x < tb.Rect.Dx(); x++ {
|
||||
expected := grays[16*y+x]
|
||||
got := tb.GrayAt(x, y)
|
||||
if expected != got {
|
||||
t.Errorf("Expected %02x at (%d,%d), got %02x", expected, x, y, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertGrayToSelf(t *testing.T) {
|
||||
for _, c := range []Gray{White, LightGray, DarkGray, Black} {
|
||||
r, g, b, a := c.RGBA()
|
||||
gray := convert(color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)})
|
||||
if gray != c {
|
||||
t.Errorf("Converting '%v' to uint16(%v,%v,%v,%v) and back to gray yields different gray '%v'", c, r, g, b, a, gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue