From 8284066f768341ccde26eeb6a2a55241580b18f4 Mon Sep 17 00:00:00 2001 From: Carl Henrik Lunde Date: Thu, 16 Apr 2020 00:31:34 +0200 Subject: [PATCH] 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. --- .../devices/epd/image2bit/image2bit.go | 169 +++++++++++++++++ .../devices/epd/image2bit/image2bit_test.go | 172 ++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 experimental/devices/epd/image2bit/image2bit.go create mode 100644 experimental/devices/epd/image2bit/image2bit_test.go diff --git a/experimental/devices/epd/image2bit/image2bit.go b/experimental/devices/epd/image2bit/image2bit.go new file mode 100644 index 0000000..4f74276 --- /dev/null +++ b/experimental/devices/epd/image2bit/image2bit.go @@ -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{} diff --git a/experimental/devices/epd/image2bit/image2bit_test.go b/experimental/devices/epd/image2bit/image2bit_test.go new file mode 100644 index 0000000..c66a631 --- /dev/null +++ b/experimental/devices/epd/image2bit/image2bit_test.go @@ -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) + } + } +}