Browse Source

Initial import

master v0.1.0
maze 10 months ago
parent
commit
f08be7f0f2
19 changed files with 2017 additions and 0 deletions
  1. +5
    -0
      .gitignore
  2. +215
    -0
      bitmap.go
  3. +83
    -0
      bitmap_test.go
  4. +101
    -0
      color.go
  5. +220
    -0
      draw.go
  6. +406
    -0
      draw_test.go
  7. +199
    -0
      font.go
  8. +260
    -0
      font_glcd_5x8.go
  9. +69
    -0
      font_test.go
  10. +85
    -0
      fx.go
  11. +77
    -0
      fx_bit.go
  12. +193
    -0
      fx_test.go
  13. +8
    -0
      go.mod
  14. +5
    -0
      go.sum
  15. BIN
      testdata/16bit.ttf
  16. BIN
      testdata/8bit.ttf
  17. BIN
      testdata/pixelmix.ttf
  18. +47
    -0
      util.go
  19. +44
    -0
      util_test.go

+ 5
- 0
.gitignore View File

@ -15,3 +15,8 @@
# Dependency directories (remove the comment below to include it)
# vendor/
# IDE
/.idea
# Test artifacts
/*.png

+ 215
- 0
bitmap.go View File

@ -0,0 +1,215 @@
package bitmap
import (
"encoding/binary"
"errors"
"image"
"image/color"
"image/draw"
)
type Format int
const (
UnknownFormat Format = iota
MHMSBFormat
MVLSBFormat
RGB565Format
RGB888Format
)
func (f Format) ColorModel() color.Model {
switch f {
case MHMSBFormat, MVLSBFormat:
return BitModel
case RGB565Format:
return RGB565Model
case RGB888Format:
return RGB888Model
default:
return color.RGBAModel
}
}
type Image interface {
draw.Image
}
type Bitmap struct {
Rect image.Rectangle
Pix []byte
Stride int
Format Format
}
func NewBitmap(w, h int) *Bitmap {
stride := w >> 3
if w&0b111 != 0 {
stride++
}
pix := stride * h
return &Bitmap{
Rect: image.Rectangle{Max: image.Point{X: w, Y: h}},
Pix: make([]byte, pix),
Stride: stride,
Format: MVLSBFormat,
}
}
func (b *Bitmap) ColorModel() color.Model {
return b.Format.ColorModel()
}
func (b *Bitmap) Bounds() image.Rectangle {
return b.Rect
}
func (b *Bitmap) At(x, y int) color.Color {
if x < 0 || y < 0 || x >= b.Rect.Max.X || y >= b.Rect.Max.Y {
return Off
}
offset, mask := b.PixOffset(x, y)
if offset >= len(b.Pix) {
return Off
}
return Bit(b.Pix[offset]&byte(mask) != 0)
}
func (b *Bitmap) PixOffset(x, y int) (int, uint) {
if b.Format == MVLSBFormat {
offset := (y>>3)*b.Stride + x
bit := uint(y & 7)
return offset, 1 << bit
}
offset := (y*b.Stride + x) >> 3
bit := uint(7 - (y & 7))
return offset, 1 << bit
}
func (b *Bitmap) Set(x, y int, c color.Color) {
b.SetBit(x, y, convertBitColor(c))
}
func (b *Bitmap) SetBit(x, y int, c Bit) {
if x < 0 || y < 0 || x >= b.Rect.Max.X || y >= b.Rect.Max.Y {
return
}
offset, mask := b.PixOffset(x, y)
if c {
b.Pix[offset] |= byte(mask)
} else {
b.Pix[offset] &= ^byte(mask)
}
}
type RGBImage struct {
Rect image.Rectangle
Pix []byte
Stride int
}
type RGB565Image struct {
Rect image.Rectangle
Pix []byte
Stride int
}
func (b *RGB565Image) Bounds() image.Rectangle {
return b.Rect
}
func (b *RGB565Image) At(x, y int) color.Color {
if !(image.Point{X: x, Y: y}).In(b.Rect) {
return color.Black
}
return RGB565(binary.BigEndian.Uint16(b.Pix[b.OffsetOf(x, y):]))
}
func (b *RGB565Image) Set(x, y int, c color.Color) {
if !(image.Point{X: x, Y: y}).In(b.Rect) {
return
}
binary.BigEndian.PutUint16(b.Pix[b.OffsetOf(x, y):], uint16(convertRGB565Color(c)))
}
func (b *RGB565Image) OffsetOf(x, y int) (offset int) {
return y*b.Stride + x*2
}
func (RGB565Image) ColorModel() color.Model {
return RGB565Model
}
func NewRGB565(w, h int) *RGB565Image {
return &RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: w, Y: h}},
Pix: make([]byte, w*h*2),
Stride: w * 2,
}
}
type RGB888Image struct {
Rect image.Rectangle
Pix []byte
Stride int
}
func NewRGB888(w, h int) *RGB888Image {
return &RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: w, Y: h}},
Pix: make([]byte, w*h*3),
Stride: w * 3,
}
}
func (b *RGB888Image) Bounds() image.Rectangle {
return b.Rect
}
func (b *RGB888Image) At(x, y int) color.Color {
if !(image.Point{X: x, Y: y}).In(b.Rect) {
return color.Black
}
var c RGB888
copy(c[:], b.Pix[b.OffsetOf(x, y):])
return c
}
func (b *RGB888Image) Set(x, y int, c color.Color) {
if !(image.Point{X: x, Y: y}).In(b.Rect) {
return
}
v := convertRGB888Color(c)
copy(b.Pix[b.OffsetOf(x, y):], v[:])
}
func (b *RGB888Image) OffsetOf(x, y int) (offset int) {
return y*b.Stride + x*3
}
func (b *RGB888Image) ColorModel() color.Model {
return RGB888Model
}
func New(width, height int, format Format) (Image, error) {
switch format {
case MHMSBFormat, MVLSBFormat:
b := NewBitmap(width, height)
b.Format = format
return b, nil
case RGB565Format:
return NewRGB565(width, height), nil
case RGB888Format:
return NewRGB888(width, height), nil
default:
return nil, errors.New("framebuffer: invalid format")
}
}
func Must(width, height int, format Format) Image {
b, err := New(width, height, format)
if err != nil {
panic(err)
}
return b
}

+ 83
- 0
bitmap_test.go View File

@ -0,0 +1,83 @@
package bitmap
import (
"fmt"
"testing"
)
func TestNewBitBuffer(t *testing.T) {
tests := []struct {
W, H int
WantStride int
WantPixLen int
}{
{1, 1, 1, 1},
{3, 3, 1, 3},
{8, 8, 1, 8},
{128, 32, 16, 512},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%dx%d", test.W, test.H), func(it *testing.T) {
v := NewBitmap(test.W, test.H)
if v == nil {
it.Fatal("NewBitmap returned nil")
}
if v.Stride != test.WantStride {
it.Errorf("expected stride %d, got %d", test.WantStride, v.Stride)
}
if l := len(v.Pix); l != test.WantPixLen {
it.Errorf("expected pix len %d, got %d", test.WantPixLen, l)
}
})
}
}
func TestNewRGB565Buffer(t *testing.T) {
tests := []struct {
W, H int
WantStride int
WantPixLen int
}{
{8, 8, 16, 128},
{128, 32, 256, 8192},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%dx%d", test.W, test.H), func(it *testing.T) {
v := NewRGB565(test.W, test.H)
if v == nil {
it.Fatal("NewRGB565 returned nil")
}
if v.Stride != test.WantStride {
it.Errorf("expected stride %d, got %d", test.WantStride, v.Stride)
}
if l := len(v.Pix); l != test.WantPixLen {
it.Errorf("expected pix len %d, got %d", test.WantPixLen, l)
}
})
}
}
func TestNewRGB888Buffer(t *testing.T) {
tests := []struct {
W, H int
WantStride int
WantPixLen int
}{
{8, 8, 24, 192},
{128, 32, 384, 12288},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%dx%d", test.W, test.H), func(it *testing.T) {
v := NewRGB888(test.W, test.H)
if v == nil {
it.Fatal("NewRGB888 returned nil")
}
if v.Stride != test.WantStride {
it.Errorf("expected stride %d, got %d", test.WantStride, v.Stride)
}
if l := len(v.Pix); l != test.WantPixLen {
it.Errorf("expected pix len %d, got %d", test.WantPixLen, l)
}
})
}
}

+ 101
- 0
color.go View File

@ -0,0 +1,101 @@
package bitmap
import "image/color"
// Color models supported by this package.
var (
BitModel = color.ModelFunc(convertBit)
RGB565Model = color.ModelFunc(convertRGB565)
RGB888Model = color.ModelFunc(convertRGB888)
)
// Bit is a 1-bit color.
type Bit bool
// Bit values.
const (
Off Bit = false
On Bit = true
)
func (c Bit) RGBA() (r, g, b, a uint32) {
if c {
return 0xffff, 0xffff, 0xffff, 0xffff
}
return 0, 0, 0, 0xffff
}
func convertBit(c color.Color) color.Color {
return convertBitColor(c)
}
func convertBitColor(c color.Color) Bit {
switch c := c.(type) {
case Bit:
return c
default:
r, g, b, _ := c.RGBA()
return (r | g | b) >= 0x8000
}
}
// RGB565 is a 16-bit RGB color with no alpha channel.
type RGB565 uint16 // 5-6-5 RGB
func (c RGB565) RGBA() (r, g, b, a uint32) {
// To convert a color channel from 5 or 6 bits back to 16 bits, the short
// bit pattern is duplicated to fill all 16 bits.
// For example the green channel in rgb565 is the middle 6 bits:
// 00000GGGGGG00000
//
// To create a 16 bit channel, these bits are or-ed together starting at the
// highest bit:
// GGGGGG0000000000 shifted << 5
// 000000GGGGGG0000 shifted >> 1
// 000000000000GGGG shifted >> 7
//
// These patterns map the minimum (all bits 0) and maximum (all bits 1)
// 5 and 6 bit channel values to the minimum and maximum 16 bit channel
// values.
//
// Alpha is always 100% opaque since this model does not support
// transparency.
rBits := uint32(c & 0xF800) // RRRRR00000000000
gBits := uint32(c & 0x7E0) // 00000GGGGGG00000
bBits := uint32(c & 0x1F) // 00000000000BBBBB
r = uint32(rBits | rBits>>5 | rBits>>10 | rBits>>15)
g = uint32(gBits<<5 | gBits>>1 | gBits>>7)
b = uint32(bBits<<11 | bBits<<6 | bBits<<1 | bBits>>4)
a = 0xffff
return
}
func convertRGB565(c color.Color) color.Color {
return convertRGB565Color(c)
}
func convertRGB565Color(c color.Color) RGB565 {
r, g, b, _ := c.RGBA()
// RRRRRGGGGGGBBBBB
return RGB565((r & 0xf800) + ((g & 0xfc00) >> 5) + ((b & 0xf800) >> 11))
}
// RGB888 is a 24-bit color with no alpha channel.
type RGB888 [3]byte // 8-8-8 RGB
func (c RGB888) RGBA() (r, g, b, a uint32) {
r = uint32(c[0]) | uint32(c[0])<<8
g = uint32(c[1]) | uint32(c[1])<<8
b = uint32(c[2]) | uint32(c[2])<<8
a = 0xffff
return
}
func convertRGB888(c color.Color) color.Color {
return convertRGB888Color(c)
}
func convertRGB888Color(c color.Color) RGB888 {
r, g, b, _ := c.RGBA()
return RGB888{byte(r), byte(g), byte(b)}
}

+ 220
- 0
draw.go View File

@ -0,0 +1,220 @@
package bitmap
import (
"encoding/binary"
"image"
"image/color"
)
func Fill(b Image, c color.Color) {
switch b := b.(type) {
case *Bitmap:
if convertBitColor(c) {
memset(b.Pix, 0xff)
} else {
memset(b.Pix, 0x00)
}
case *RGB565Image:
var v [2]byte
binary.BigEndian.PutUint16(v[:], uint16(convertRGB565Color(c)))
memsetSlice(b.Pix, v[:])
case *RGB888Image:
v := convertRGB888Color(c)
memsetSlice(b.Pix, v[:])
default:
// Naive and slow set pixel implementation.
r := b.Bounds()
for y := r.Min.Y; y < r.Max.Y; y++ {
for x := r.Min.X; x < r.Max.X; x++ {
b.Set(x, y, c)
}
}
}
}
// FillRectangle draws a filled rectangle between the two points.
func FillRectangle(b Image, r image.Rectangle, c color.Color) {
fillRectangle(b, r.Min.X, r.Min.Y, r.Max.X-r.Min.X, r.Max.Y-r.Min.Y, c)
}
func fillRectangle(b Image, x, y, width, height int, c color.Color) {
switch b := b.(type) {
case *Bitmap:
switch b.Format {
case MHMSBFormat:
var v byte
if convertBitColor(c) {
v = 0x01
}
for xx := x; xx < x+width; xx++ {
offset := 7 - xx&0x7
for yy := y; yy < y+height; yy++ {
index := yy*b.Stride + xx
b.Pix[index] = (b.Pix[index] & ^(0x01 << offset)) | v<<offset
}
}
return
case MVLSBFormat:
var v byte
if convertBitColor(c) {
v = 0x01
}
for ; height > 0; height, y = height-1, y+1 {
index := (y>>3)*b.Stride + x
offset := y & 0x07
for w := 0; w < width; w++ {
b.Pix[index+w] = (b.Pix[index+w] & ^(0x01 << offset)) | v<<offset
}
}
return
default:
}
case *RGB565Image:
var (
r = b.Bounds()
xe = min(x+width, r.Max.X)
ye = min(y+height, r.Max.Y)
c = convertRGB565Color(c)
v [2]byte
)
binary.BigEndian.PutUint16(v[:], uint16(c))
for ; y < ye; y++ {
o := y*b.Stride + x*2
memsetSlice(b.Pix[o:o+(xe-x)*2], v[:])
}
case *RGB888Image:
var (
r = b.Bounds()
xe = min(x+width, r.Max.X)
ye = min(y+height, r.Max.Y)
v = convertRGB888Color(c)
)
for ; y < ye; y++ {
o := y*b.Stride + x*3
memsetSlice(b.Pix[o:o+(xe-x)*3], v[:])
}
default:
// Naive pixel by pixel fill.
var (
r = b.Bounds()
xe = min(x+width, r.Max.X)
ye = min(y+height, r.Max.Y)
)
for ; y < ye; y++ {
for ; x < xe; x++ {
b.Set(x, y, c)
}
}
}
}
// Rectangle draws a rectangle outline between the two points.
func Rectangle(b Image, r image.Rectangle, c color.Color) {
rectangle(b, r.Min.X, r.Min.Y, r.Dx(), r.Dy(), c, false)
}
func rectangle(b Image, x, y, width, height int, c color.Color, fill bool) {
r := b.Bounds()
if width < 1 || height < 1 || (x+width) <= 0 || (y+height) <= 0 || y >= r.Max.Y || x >= r.Max.X {
return
}
var (
xe = min(r.Max.X-1, x+width-1)
ye = min(r.Max.Y-1, y+height-1)
)
x = max(x, 0)
y = max(y, 0)
if fill || height == 1 || width == 1 {
fillRectangle(b, x, y, xe-x+1, ye-y+1, c)
} else {
fillRectangle(b, x, y, xe-x+1, 1, c)
fillRectangle(b, x, y, 1, ye-y+1, c)
fillRectangle(b, x, ye, xe-x+1, 1, c)
fillRectangle(b, xe, y, 1, ye-y+1, c)
}
}
// HLine draws a horizontal line at p with the given width.
func HLine(b Image, p image.Point, width int, c color.Color) {
rectangle(b, p.X, p.Y, width, 1, c, false)
}
// VLine draws a vertical line at p with the given height.
func VLine(b Image, p image.Point, height int, c color.Color) {
rectangle(b, p.X, p.Y, 1, height, c, false)
}
// Line draws a line between two points.
func Line(b Image, p0, p1 image.Point, c color.Color) {
var (
dx = abs(p1.X - p0.X)
dy = abs(p1.Y - p0.Y)
x, y = p0.X, p0.Y
sx, sy = -1, -1
)
if p0.X <= p1.X {
sx = 1
}
if p0.Y <= p1.Y {
sy = 1
}
if dx > dy {
e := float64(dx) / 2.0
for x != p1.X {
b.Set(x, y, c)
e -= float64(dy)
if e < 0 {
y += sy
e += float64(dx)
}
x += sx
}
} else {
e := float64(dy) / 2.0
for y != p1.Y {
b.Set(x, y, c)
e -= float64(dx)
if e < 0 {
x += sx
e += float64(dy)
}
y += sy
}
}
b.Set(x, y, c)
}
// Circle draws a circle at point p with radius r.
func Circle(b Image, p image.Point, r int, c color.Color) {
if r < 0 {
return
}
// Bresenham midpoint circle algorithm.
var (
x1, y1, err = -r, 0, 2 - 2*r
x, y = p.X, p.Y
)
for {
b.Set(x-x1, y+y1, c)
b.Set(x-y1, y-x1, c)
b.Set(x+x1, y-y1, c)
b.Set(x+y1, y+x1, c)
r = err
if r > x1 {
x1++
err += x1*2 + 1
}
if r <= y1 {
y1++
err += y1*2 + 1
}
if x1 >= 0 {
break
}
}
}

+ 406
- 0
draw_test.go View File

@ -0,0 +1,406 @@
package bitmap
import (
"image"
"image/color"
"reflect"
"testing"
)
var testColor = color.RGBA{R: 0x55, G: 0xaa, B: 0xff, A: 0x33}
func TestFill(t *testing.T) {
tests := []struct {
Test Image
Want Image
}{
{
NewBitmap(8, 8),
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
Stride: 1,
Format: MVLSBFormat,
},
},
{
NewRGB565(5, 5),
&RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f,
0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f,
0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f,
0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f,
0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f,
},
Stride: 10,
},
},
{
NewRGB888(3, 3),
&RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: 3, Y: 3}},
Pix: []byte{
0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff,
0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff,
0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff,
},
Stride: 9,
},
},
}
for _, test := range tests {
t.Run(reflect.TypeOf(test.Test).Elem().String(), func(it *testing.T) {
Fill(test.Test, testColor)
if !reflect.DeepEqual(test.Test, test.Want) {
it.Fatalf("expected %T:\n%+v\ngot %T:\n%+v", test.Want, test.Want, test.Test, test.Test)
}
})
}
}
func TestFillRectangle(t *testing.T) {
tests := []struct {
Test Image
Want Image
}{
{
NewBitmap(8, 8),
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0b00000011,
0b00000011,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
},
Stride: 1,
Format: MVLSBFormat,
},
},
{
NewRGB565(5, 5),
&RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x55, 0x5f, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0x5f, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 10,
},
},
{
NewRGB888(3, 3),
&RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: 3, Y: 3}},
Pix: []byte{
0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00,
0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 9,
},
},
}
for _, test := range tests {
t.Run(reflect.TypeOf(test.Test).Elem().String(), func(it *testing.T) {
FillRectangle(test.Test, image.Rect(0, 0, 2, 2), testColor)
if !reflect.DeepEqual(test.Test, test.Want) {
it.Fatalf("expected %T:\n%+v\ngot %T:\n%+v", test.Want, test.Want, test.Test, test.Test)
}
})
}
}
func TestHLine(t *testing.T) {
tests := []struct {
Test Image
Want Image
}{
{
NewBitmap(8, 8),
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0b00000000,
0b00000100,
0b00000100,
0b00000100,
0b00000100,
0b00000100,
0b00000100,
0b00000100,
},
Stride: 1,
Format: MVLSBFormat,
},
},
{
NewRGB565(5, 5),
&RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 10,
},
},
{
NewRGB888(5, 5),
&RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 15,
},
},
}
for _, test := range tests {
t.Run(reflect.TypeOf(test.Test).Elem().String(), func(it *testing.T) {
HLine(test.Test, image.Pt(1, 2), 10, testColor)
if !reflect.DeepEqual(test.Test, test.Want) {
it.Fatalf("expected %T:\n%+v\ngot %T:\n%+v", test.Want, test.Want, test.Test, test.Test)
}
})
}
}
func TestVLine(t *testing.T) {
tests := []struct {
Test Image
Want Image
}{
{
NewBitmap(8, 8),
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0b00000000,
0b11111100,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
},
Stride: 1,
Format: MVLSBFormat,
},
},
{
NewRGB565(5, 5),
&RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 10,
},
},
{
NewRGB888(5, 5),
&RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 15,
},
},
}
for _, test := range tests {
t.Run(reflect.TypeOf(test.Test).Elem().String(), func(it *testing.T) {
VLine(test.Test, image.Pt(1, 2), 10, testColor)
if !reflect.DeepEqual(test.Test, test.Want) {
it.Fatalf("expected %T:\n%+v\ngot %T:\n%+v", test.Want, test.Want, test.Test, test.Test)
}
})
}
}
func TestLine(t *testing.T) {
tests := []struct {
Test Image
Want Image
}{
{
NewBitmap(8, 8),
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0b00000000,
0b00000010,
0b00000100,
0b00001000,
0b00010000,
0b00100000,
0b01000000,
0b10000000,
},
Stride: 1,
Format: MVLSBFormat,
},
},
{
NewRGB565(5, 5),
&RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x5f, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x5f,
},
Stride: 10,
},
},
{
NewRGB888(5, 5),
&RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: 5, Y: 5}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff,
},
Stride: 15,
},
},
}
for _, test := range tests {
t.Run(reflect.TypeOf(test.Test).Elem().String(), func(it *testing.T) {
Line(test.Test, image.Pt(1, 1), image.Pt(10, 10), testColor)
if !reflect.DeepEqual(test.Test, test.Want) {
it.Fatalf("expected %T:\n%+v\ngot %T:\n%+v", test.Want, test.Want, test.Test, test.Test)
}
})
}
}
func TestCircle(t *testing.T) {
tests := []struct {
Test Image
Want Image
}{
{
NewBitmap(8, 8),
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0b00000000,
0b01110000,
0b10001100,
0b00000100,
0b00000010,
0b00000010,
0b00000010,
0b00000100,
},
Stride: 1,
Format: MVLSBFormat,
},
},
{
NewRGB565(8, 8),
&RGB565Image{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x5f, 0x55, 0x5f, 0x55, 0x5f, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x55, 0x5f, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x5f,
0x00, 0x00, 0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x55, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 16,
},
},
{
NewRGB888(8, 8),
&RGB888Image{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x55, 0xaa, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0xff, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
Stride: 24,
},
},
}
for _, test := range tests {
t.Run(reflect.TypeOf(test.Test).Elem().String(), func(it *testing.T) {
Circle(test.Test, image.Pt(5, 5), 4, testColor)
if !reflect.DeepEqual(test.Test, test.Want) {
it.Fatalf("expected %T:\n%+v\ngot %T:\n%+v", test.Want, test.Want, test.Test, test.Test)
}
})
}
}
/*
// Only used for debugging tests:
func testOutput(t *testing.T, i Image) {
t.Helper()
f, err := os.Create(reflect.TypeOf(i).Elem().String() + ".png")
if err != nil {
t.Error(err)
return
}
if err = png.Encode(f, i); err != nil {
_ = f.Close()
_ = os.Remove(f.Name())
t.Error(err)
return
}
if err = f.Close(); err != nil {
t.Error(err)
return
}
t.Logf("saved as %s", f.Name())
}
*/

+ 199
- 0
font.go View File

@ -0,0 +1,199 @@
package bitmap
import (
"image"
"image/draw"
"io"
"io/ioutil"
"log"
"os"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
// Builtin fonts.
var (
GLCD5x8 = NewFont(5, 8, fontGLCD5x8)
)
// Font can render glyph images.
type Font interface {
// Glyph returns the image mask for the requested glyph.
Glyph(r rune) image.Image
// Bounds is the bounding box that fits any glyph in the font.
Bounds() image.Rectangle
}
type cachedFont struct {
Font
cache map[rune]image.Image
}
func (c *cachedFont) Glyph(r rune) image.Image {
if _, ok := c.cache[r]; !ok {
c.cache[r] = c.Font.Glyph(r)
}
return c.cache[r]
}
// NewFontCache is a helper that caches all rendered glyphs in memory.
func NewFontCache(font Font) Font {
return &cachedFont{
Font: font,
cache: make(map[rune]image.Image),
}
}
// TexturedFont applies a texture to font bitmaps in the output glyph.
type TexturedFont struct {
Font
Texture image.Image
}
func (f TexturedFont) Glyph(r rune) image.Image {
var (
b = f.Bounds()
glyph = image.NewRGBA(b)
mask = f.Font.Glyph(r)
)
if mask != nil && f.Texture != nil {
draw.DrawMask(glyph, b, f.Texture, image.Point{}, mask, image.Point{}, draw.Src)
}
return glyph
}
type bitmapFont struct {
rect image.Rectangle
stride int
pix []byte
}
func (f *bitmapFont) Glyph(r rune) image.Image {
o := int(r) * f.stride
if o < 0 || o >= len(f.pix) {
return nil
}
b := NewBitmap(f.rect.Max.X, f.rect.Max.Y)
for x := 0; x < f.rect.Max.X; x++ {
v := f.pix[o+x]
for y := 0; y < f.rect.Max.Y; y++ {
b.Set(x, y, Bit((v>>y)&0x01 == 0x01))
}
}
return b
}
func (f *bitmapFont) Bounds() image.Rectangle {
return f.rect
}
func NewFont(w, h int, pix []byte) Font {
return &bitmapFont{
rect: image.Rectangle{Max: image.Point{X: w, Y: h}},
stride: w,
pix: pix,
}
}
func LoadFont(name string) (Font, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
var dim [2]byte
if _, err = io.ReadFull(f, dim[:]); err != nil {
_ = f.Close()
return nil, err
}
var pix []byte
if pix, err = ioutil.ReadAll(f); err != nil {
_ = f.Close()
return nil, err
}
if err = f.Close(); err != nil {
return nil, err
}
return NewFont(int(dim[0]), int(dim[1]), pix), nil
}
type ttfFont struct {
face font.Face
rect image.Rectangle
}
func (f ttfFont) Glyph(r rune) image.Image {
/*
var (
size = measureRune(f.face, r)
glyph = NewBitmap(size.X, size.Y)
//center = fixed.P((f.rect.Max.X-size.X)/2, (f.rect.Max.Y-size.Y)/2+size.Y+1)
center = fixed.P((f.rect.Max.X-size.X)/2, size.Y)
)
log.Printf("glyph %c (%d) is %dx%d", r, r, size.X, size.Y)
dr, mask, maskPoint, _, ok := f.face.Glyph(center, r)
if !ok {
return nil
}
*/
dr, mask, maskPoint, _, ok := f.face.Glyph(fixed.Point26_6{}, r)
if !ok {
return nil
}
glyph := NewBitmap(dr.Dx(), dr.Dy())
log.Printf("glyph: %s, dr: %s, mask: %s, mask point: %s", glyph.Bounds(), dr, mask.Bounds(), maskPoint)
draw.DrawMask(glyph, glyph.Bounds(), image.White, image.Point{}, mask, maskPoint, draw.Over)
return glyph
}
func (f ttfFont) Bounds() image.Rectangle {
return f.rect
}
func NewTTFFont(ttf []byte, size int) (Font, error) {
f, err := truetype.Parse(ttf)
if err != nil {
return nil, err
}
face := truetype.NewFace(f, &truetype.Options{
Size: float64(size),
DPI: 72, // At 72 DPI, px == pt
Hinting: font.HintingNone,
})
return &ttfFont{
face: face,
rect: measureFont(face, size),
}, nil
}
func LoadTTFFont(name string, size int) (Font, error) {
ttf, err := ioutil.ReadFile(name)
if err != nil {
return nil, err
}
return NewTTFFont(ttf, size)
}
func measureFont(face font.Face, height int) (b image.Rectangle) {
b.Max.Y = height
const testGlyphs = "xXmMjJ0"
for _, r := range testGlyphs {
size := measureRune(face, r)
b.Max.X = max(b.Max.X, size.X)
b.Max.Y = max(b.Max.Y, size.Y)
}
return
}
func measureRune(face font.Face, r rune) image.Point {
bounds, advance, ok := face.GlyphBounds(r)
if !ok {
return image.Point{}
}
return image.Pt(advance.Ceil(), (bounds.Max.Y - bounds.Min.Y).Ceil())
}

+ 260
- 0
font_glcd_5x8.go View File

@ -0,0 +1,260 @@
package bitmap
var fontGLCD5x8 = []byte{
0x00, 0x00, 0x00, 0x00, 0x00,
0x3E, 0x5B, 0x4F, 0x5B, 0x3E,
0x3E, 0x6B, 0x4F, 0x6B, 0x3E,
0x1C, 0x3E, 0x7C, 0x3E, 0x1C,
0x18, 0x3C, 0x7E, 0x3C, 0x18,
0x1C, 0x57, 0x7D, 0x57, 0x1C,
0x1C, 0x5E, 0x7F, 0x5E, 0x1C,
0x00, 0x18, 0x3C, 0x18, 0x00,
0xFF, 0xE7, 0xC3, 0xE7, 0xFF,
0x00, 0x18, 0x24, 0x18, 0x00,
0xFF, 0xE7, 0xDB, 0xE7, 0xFF,
0x30, 0x48, 0x3A, 0x06, 0x0E,
0x26, 0x29, 0x79, 0x29, 0x26,
0x40, 0x7F, 0x05, 0x05, 0x07,
0x40, 0x7F, 0x05, 0x25, 0x3F,
0x5A, 0x3C, 0xE7, 0x3C, 0x5A,
0x7F, 0x3E, 0x1C, 0x1C, 0x08,
0x08, 0x1C, 0x1C, 0x3E, 0x7F,
0x14, 0x22, 0x7F, 0x22, 0x14,
0x5F, 0x5F, 0x00, 0x5F, 0x5F,
0x06, 0x09, 0x7F, 0x01, 0x7F,
0x00, 0x66, 0x89, 0x95, 0x6A,
0x60, 0x60, 0x60, 0x60, 0x60,
0x94, 0xA2, 0xFF, 0xA2, 0x94,
0x08, 0x04, 0x7E, 0x04, 0x08,
0x10, 0x20, 0x7E, 0x20, 0x10,
0x08, 0x08, 0x2A, 0x1C, 0x08,
0x08, 0x1C, 0x2A, 0x08, 0x08,
0x1E, 0x10, 0x10, 0x10, 0x10,
0x0C, 0x1E, 0x0C, 0x1E, 0x0C,
0x30, 0x38, 0x3E, 0x38, 0x30,
0x06, 0x0E, 0x3E, 0x0E, 0x06,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x5F, 0x00, 0x00,
0x00, 0x07, 0x00, 0x07, 0x00,
0x14, 0x7F, 0x14, 0x7F, 0x14,
0x24, 0x2A, 0x7F, 0x2A, 0x12,
0x23, 0x13, 0x08, 0x64, 0x62,
0x36, 0x49, 0x56, 0x20, 0x50,
0x00, 0x08, 0x07, 0x03, 0x00,
0x00, 0x1C, 0x22, 0x41, 0x00,
0x00, 0x41, 0x22, 0x1C, 0x00,
0x2A, 0x1C, 0x7F, 0x1C, 0x2A,
0x08, 0x08, 0x3E, 0x08, 0x08,
0x00, 0x80, 0x70, 0x30, 0x00,
0x08, 0x08, 0x08, 0x08, 0x08,
0x00, 0x00, 0x60, 0x60, 0x00,
0x20, 0x10, 0x08, 0x04, 0x02,
0x3E, 0x51, 0x49, 0x45, 0x3E,
0x00, 0x42, 0x7F, 0x40, 0x00,
0x72, 0x49, 0x49, 0x49, 0x46,
0x21, 0x41, 0x49, 0x4D, 0x33,
0x18, 0x14, 0x12, 0x7F, 0x10,
0x27, 0x45, 0x45, 0x45, 0x39,
0x3C, 0x4A, 0x49, 0x49, 0x31,
0x41, 0x21, 0x11, 0x09, 0x07,
0x36, 0x49, 0x49, 0x49, 0x36,
0x46, 0x49, 0x49, 0x29, 0x1E,
0x00, 0x00, 0x14, 0x00, 0x00,
0x00, 0x40, 0x34, 0x00, 0x00,
0x00, 0x08, 0x14, 0x22, 0x41,
0x14, 0x14, 0x14, 0x14, 0x14,
0x00, 0x41, 0x22, 0x14, 0x08,
0x02, 0x01, 0x59, 0x09, 0x06,
0x3E, 0x41, 0x5D, 0x59, 0x4E,
0x7C, 0x12, 0x11, 0x12, 0x7C,
0x7F, 0x49, 0x49, 0x49, 0x36,
0x3E, 0x41, 0x41, 0x41, 0x22,
0x7F, 0x41, 0x41, 0x41, 0x3E,
0x7F, 0x49, 0x49, 0x49, 0x41,
0x7F, 0x09, 0x09, 0x09, 0x01,
0x3E, 0x41, 0x41, 0x51, 0x73,
0x7F, 0x08, 0x08, 0x08, 0x7F,
0x00, 0x41, 0x7F, 0x41, 0x00,
0x20, 0x40, 0x41, 0x3F, 0x01,
0x7F, 0x08, 0x14, 0x22, 0x41,
0x7F, 0x40, 0x40, 0x40, 0x40,
0x7F, 0x02, 0x1C, 0x02, 0x7F,
0x7F, 0x04, 0x08, 0x10, 0x7F,
0x3E, 0x41, 0x41, 0x41, 0x3E,
0x7F, 0x09, 0x09, 0x09, 0x06,
0x3E, 0x41, 0x51, 0x21, 0x5E,
0x7F, 0x09, 0x19, 0x29, 0x46,
0x26, 0x49, 0x49, 0x49, 0x32,
0x03, 0x01, 0x7F, 0x01, 0x03,
0x3F, 0x40, 0x40, 0x40, 0x3F,
0x1F, 0x20, 0x40, 0x20, 0x1F,
0x3F, 0x40, 0x38, 0x40, 0x3F,
0x63, 0x14, 0x08, 0x14, 0x63,
0x03, 0x04, 0x78, 0x04, 0x03,
0x61, 0x59, 0x49, 0x4D, 0x43,
0x00, 0x7F, 0x41, 0x41, 0x41,
0x02, 0x04, 0x08, 0x10, 0x20,
0x00, 0x41, 0x41, 0x41, 0x7F,
0x04, 0x02, 0x01, 0x02, 0x04,
0x40, 0x40, 0x40, 0x40, 0x40,
0x00, 0x03, 0x07, 0x08, 0x00,
0x20, 0x54, 0x54, 0x78, 0x40,
0x7F, 0x28, 0x44, 0x44, 0x38,
0x38, 0x44, 0x44, 0x44, 0x28,
0x38, 0x44, 0x44, 0x28, 0x7F,
0x38, 0x54, 0x54, 0x54, 0x18,
0x00, 0x08, 0x7E, 0x09, 0x02,
0x18, 0xA4, 0xA4, 0x9C, 0x78,
0x7F, 0x08, 0x04, 0x04, 0x78,
0x00, 0x44, 0x7D, 0x40, 0x00,
0x20, 0x40, 0x40, 0x3D, 0x00,
0x7F, 0x10, 0x28, 0x44, 0x00,
0x00, 0x41, 0x7F, 0x40, 0x00,
0x7C, 0x04, 0x78, 0x04, 0x78,
0x7C, 0x08, 0x04, 0x04, 0x78,
0x38, 0x44, 0x44, 0x44, 0x38,
0xFC, 0x18, 0x24, 0x24, 0x18,
0x18, 0x24, 0x24, 0x18, 0xFC,
0x7C, 0x08, 0x04, 0x04, 0x08,
0x48, 0x54, 0x54, 0x54, 0x24,
0x04, 0x04, 0x3F, 0x44, 0x24,
0x3C, 0x40, 0x40, 0x20, 0x7C,
0x1C, 0x20, 0x40, 0x20, 0x1C,
0x3C, 0x40, 0x30, 0x40, 0x3C,
0x44, 0x28, 0x10, 0x28, 0x44,
0x4C, 0x90, 0x90, 0x90, 0x7C,
0x44, 0x64, 0x54, 0x4C, 0x44,
0x00, 0x08, 0x36, 0x41, 0x00,
0x00, 0x00, 0x77, 0x00, 0x00,
0x00, 0x41, 0x36, 0x08, 0x00,
0x02, 0x01, 0x02, 0x04, 0x02,
0x3C, 0x26, 0x23, 0x26, 0x3C,
0x1E, 0xA1, 0xA1, 0x61, 0x12,
0x3A, 0x40, 0x40, 0x20, 0x7A,
0x38, 0x54, 0x54, 0x55, 0x59,
0x21, 0x55, 0x55, 0x79, 0x41,
0x22, 0x54, 0x54, 0x78, 0x42, // a-umlaut
0x21, 0x55, 0x54, 0x78, 0x40,
0x20, 0x54, 0x55, 0x79, 0x40,
0x0C, 0x1E, 0x52, 0x72, 0x12,
0x39, 0x55, 0x55, 0x55, 0x59,
0x39, 0x54, 0x54, 0x54, 0x59,
0x39, 0x55, 0x54, 0x54, 0x58,
0x00, 0x00, 0x45, 0x7C, 0x41,
0x00, 0x02, 0x45, 0x7D, 0x42,
0x00, 0x01, 0x45, 0x7C, 0x40,
0x7D, 0x12, 0x11, 0x12, 0x7D, // A-umlaut
0xF0, 0x28, 0x25, 0x28, 0xF0,
0x7C, 0x54, 0x55, 0x45, 0x00,
0x20, 0x54, 0x54, 0x7C, 0x54,
0x7C, 0x0A, 0x09, 0x7F, 0x49,
0x32, 0x49, 0x49, 0x49, 0x32,
0x3A, 0x44, 0x44, 0x44, 0x3A, // o-umlaut
0x32, 0x4A, 0x48, 0x48, 0x30,
0x3A, 0x41, 0x41, 0x21, 0x7A,
0x3A, 0x42, 0x40, 0x20, 0x78,
0x00, 0x9D, 0xA0, 0xA0, 0x7D,
0x3D, 0x42, 0x42, 0x42, 0x3D, // O-umlaut
0x3D, 0x40, 0x40, 0x40, 0x3D,
0x3C, 0x24, 0xFF, 0x24, 0x24,
0x48, 0x7E, 0x49, 0x43, 0x66,
0x2B, 0x2F, 0xFC, 0x2F, 0x2B,
0xFF, 0x09, 0x29, 0xF6, 0x20,
0xC0, 0x88, 0x7E, 0x09, 0x03,
0x20, 0x54, 0x54, 0x79, 0x41,
0x00, 0x00, 0x44, 0x7D, 0x41,
0x30, 0x48, 0x48, 0x4A, 0x32,
0x38, 0x40, 0x40, 0x22, 0x7A,
0x00, 0x7A, 0x0A, 0x0A, 0x72,
0x7D, 0x0D, 0x19, 0x31, 0x7D,
0x26, 0x29, 0x29, 0x2F, 0x28,
0x26, 0x29, 0x29, 0x29, 0x26,
0x30, 0x48, 0x4D, 0x40, 0x20,
0x38, 0x08, 0x08, 0x08, 0x08,
0x08, 0x08, 0x08, 0x08, 0x38,
0x2F, 0x10, 0xC8, 0xAC, 0xBA,
0x2F, 0x10, 0x28, 0x34, 0xFA,
0x00, 0x00, 0x7B, 0x00, 0x00,
0x08, 0x14, 0x2A, 0x14, 0x22,
0x22, 0x14, 0x2A, 0x14, 0x08,
0x55, 0x00, 0x55, 0x00, 0x55, // #176 (25% block) missing in old code
0xAA, 0x55, 0xAA, 0x55, 0xAA, // 50% block
0xFF, 0x55, 0xFF, 0x55, 0xFF, // 75% block
0x00, 0x00, 0x00, 0xFF, 0x00,
0x10, 0x10, 0x10, 0xFF, 0x00,
0x14, 0x14, 0x14, 0xFF, 0x00,
0x10, 0x10, 0xFF, 0x00, 0xFF,
0x10, 0x10, 0xF0, 0x10, 0xF0,
0x14, 0x14, 0x14, 0xFC, 0x00,
0x14, 0x14, 0xF7, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0xFF,
0x14, 0x14, 0xF4, 0x04, 0xFC,
0x14, 0x14, 0x17, 0x10, 0x1F,
0x10, 0x10, 0x1F, 0x10, 0x1F,
0x14, 0x14, 0x14, 0x1F, 0x00,
0x10, 0x10, 0x10, 0xF0, 0x00,
0x00, 0x00, 0x00, 0x1F, 0x10,
0x10, 0x10, 0x10, 0x1F, 0x10,
0x10, 0x10, 0x10, 0xF0, 0x10,
0x00, 0x00, 0x00, 0xFF, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0xFF, 0x10,
0x00, 0x00, 0x00, 0xFF, 0x14,
0x00, 0x00, 0xFF, 0x00, 0xFF,
0x00, 0x00, 0x1F, 0x10, 0x17,
0x00, 0x00, 0xFC, 0x04, 0xF4,
0x14, 0x14, 0x17, 0x10, 0x17,
0x14, 0x14, 0xF4, 0x04, 0xF4,
0x00, 0x00, 0xFF, 0x00, 0xF7,
0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0xF7, 0x00, 0xF7,
0x14, 0x14, 0x14, 0x17, 0x14,
0x10, 0x10, 0x1F, 0x10, 0x1F,
0x14, 0x14, 0x14, 0xF4, 0x14,
0x10, 0x10, 0xF0, 0x10, 0xF0,
0x00, 0x00, 0x1F, 0x10, 0x1F,
0x00, 0x00, 0x00, 0x1F, 0x14,
0x00, 0x00, 0x00, 0xFC, 0x14,
0x00, 0x00, 0xF0, 0x10, 0xF0,
0x10, 0x10, 0xFF, 0x10, 0xFF,
0x14, 0x14, 0x14, 0xFF, 0x14,
0x10, 0x10, 0x10, 0x1F, 0x00,
0x00, 0x00, 0x00, 0xF0, 0x10,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF,
0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
0x38, 0x44, 0x44, 0x38, 0x44,
0xFC, 0x4A, 0x4A, 0x4A, 0x34, // sharp-s or beta
0x7E, 0x02, 0x02, 0x06, 0x06,
0x02, 0x7E, 0x02, 0x7E, 0x02,
0x63, 0x55, 0x49, 0x41, 0x63,
0x38, 0x44, 0x44, 0x3C, 0x04,
0x40, 0x7E, 0x20, 0x1E, 0x20,
0x06, 0x02, 0x7E, 0x02, 0x02,
0x99, 0xA5, 0xE7, 0xA5, 0x99,
0x1C, 0x2A, 0x49, 0x2A, 0x1C,
0x4C, 0x72, 0x01, 0x72, 0x4C,
0x30, 0x4A, 0x4D, 0x4D, 0x30,
0x30, 0x48, 0x78, 0x48, 0x30,
0xBC, 0x62, 0x5A, 0x46, 0x3D,
0x3E, 0x49, 0x49, 0x49, 0x00,
0x7E, 0x01, 0x01, 0x01, 0x7E,
0x2A, 0x2A, 0x2A, 0x2A, 0x2A,
0x44, 0x44, 0x5F, 0x44, 0x44,
0x40, 0x51, 0x4A, 0x44, 0x40,
0x40, 0x44, 0x4A, 0x51, 0x40,
0x00, 0x00, 0xFF, 0x01, 0x03,
0xE0, 0x80, 0xFF, 0x00, 0x00,
0x08, 0x08, 0x6B, 0x6B, 0x08,
0x36, 0x12, 0x36, 0x24, 0x36,
0x06, 0x0F, 0x09, 0x0F, 0x06,
0x00, 0x00, 0x18, 0x18, 0x00,
0x00, 0x00, 0x10, 0x10, 0x00,
0x30, 0x40, 0xFF, 0x01, 0x01,
0x00, 0x1F, 0x01, 0x01, 0x1E,
0x00, 0x19, 0x1D, 0x17, 0x12,
0x00, 0x3C, 0x3C, 0x3C, 0x3C,
0x00, 0x00, 0x00, 0x00, 0x00, // 255 NBSP
}

+ 69
- 0
font_test.go View File

@ -0,0 +1,69 @@
package bitmap
import (
"image"
"image/draw"
"path/filepath"
"testing"
)
func TestBitmapFont(t *testing.T) {
testFont(t, GLCD5x8, 5, 8)
}
func TestLoadTTFFont(t *testing.T) {
tests := []struct {
Name string
Size int
}{
{"16bit.ttf", 16},
{"pixelmix.ttf", 10},
}
for _, test := range tests {
t.Run(test.Name, func(it *testing.T) {
f, err := LoadTTFFont(filepath.Join("testdata", test.Name), test.Size)
if err != nil {
it.Fatal(err)
}
testFont(it, f, test.Size, test.Size)
})
}
}
func testFont(t *testing.T, f Font, w, h int) {
t.Helper()
var (
bounds = image.Rect(0, 0, (f.Bounds().Dx()+1)*4, h)
test = image.NewRGBA(bounds)