aboutsummaryrefslogtreecommitdiff
path: root/pack.go
blob: b396b239c3fe450409ec66a3ffa19d93b280c5b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package epd7in3e

import (
	"errors"
	"fmt"
	"image"
)

// Frame geometry: 800x480, 4 bits per pixel, 2 pixels per byte, left
// pixel in the HIGH nibble, row-major, top row first.
const (
	Width      = 800
	Height     = 480
	FrameBytes = Width * Height / 2 // 192000
)

// ErrBadSize is returned when an image is neither 800x480 nor
// 480x800.
var ErrBadSize = errors.New("epd7in3e: image must be 800x480 or 480x800")

// Pack writes the panel frame buffer for img into buf, which must be
// exactly FrameBytes long. img must be 800x480, or 480x800, in which
// case it is rotated 90 degrees clockwise. Any other size is an error.
func Pack(buf []byte, img image.Image) error {
	if len(buf) != FrameBytes {
		return fmt.Errorf("epd7in3e: buffer is %d bytes, want %d", len(buf), FrameBytes)
	}
	b := img.Bounds()
	switch {
	case b.Dx() == Width && b.Dy() == Height:
		packDirect(buf, img)
	case b.Dx() == Height && b.Dy() == Width:
		packRotated(buf, img)
	default:
		return fmt.Errorf("epd7in3e: image is %dx%d, want %dx%d or %dx%d: %w",
			b.Dx(), b.Dy(), Width, Height, Height, Width, ErrBadSize)
	}
	return nil
}

// Rotate180 reverses a packed frame buffer in place: both byte order
// and the nibble order within each byte, so the left pixel stays the
// high nibble after rotation.
func Rotate180(buf []byte) {
	n := len(buf)
	for i := 0; i < n/2; i++ {
		j := n - 1 - i
		buf[i], buf[j] = swapNibbles(buf[j]), swapNibbles(buf[i])
	}
}

func swapNibbles(b byte) byte { return (b&0x0f)<<4 | b>>4 }

// packDirect fills buf from an 800x480 image, with fast paths for the
// common concrete types.
func packDirect(buf []byte, img image.Image) {
	switch p := img.(type) {
	case *image.Paletted:
		packPaletted(buf, p)
	case *image.RGBA:
		packRGBA(buf, p.Pix, p.Stride, p.Rect.Min, false)
	case *image.NRGBA:
		packRGBA(buf, p.Pix, p.Stride, p.Rect.Min, true)
	default:
		packGeneric(buf, img, 0)
	}
}

// packRotated fills buf from a 480x800 image, rotating it 90 degrees
// clockwise.
func packRotated(buf []byte, img image.Image) {
	packGeneric(buf, img, 1)
}

// packGeneric fills buf pixel by pixel through image.At; rot is 1 for
// a rotated source.
func packGeneric(buf []byte, img image.Image, rot int) {
	r := img.Bounds()
	for y := 0; y < Height; y++ {
		out := buf[y*(Width/2):]
		for x := 0; x < Width/2; x++ {
			var c0, c1 Color
			if rot == 0 {
				c0 = codeOf(img.At(r.Min.X+2*x, r.Min.Y+y))
				c1 = codeOf(img.At(r.Min.X+2*x+1, r.Min.Y+y))
			} else {
				c0 = codeOf(img.At(r.Min.X+y, r.Min.Y+Width-1-2*x))
				c1 = codeOf(img.At(r.Min.X+y, r.Min.Y+Width-2-2*x))
			}
			out[x] = byte(c0)<<4 | byte(c1)
		}
	}
}

// packPaletted fills buf from a Paletted image, using a per-palette
// code table built once.
func packPaletted(buf []byte, p *image.Paletted) {
	if p.Palette == nil {
		packGeneric(buf, p, 0)
		return
	}
	var m [256]byte
	for i, c := range p.Palette {
		m[i] = byte(codeOf(c))
	}
	minX, minY := p.Rect.Min.X, p.Rect.Min.Y
	for y := 0; y < Height; y++ {
		row := p.Pix[(y+minY)*p.Stride+minX:]
		out := buf[y*(Width/2):]
		for x := 0; x < Width/2; x++ {
			out[x] = m[row[2*x]]<<4 | m[row[2*x+1]]
		}
	}
}

// packRGBA fills buf from raw 4-bytes-per-pixel pixels in RGBA (false)
// or NRGBA (true, premultiplied alpha) layout.
func packRGBA(buf []byte, pix []byte, stride int, min image.Point, premultiplied bool) {
	for y := 0; y < Height; y++ {
		i := (y+min.Y)*stride + 4*min.X
		out := buf[y*(Width/2):]
		for x := 0; x < Width/2; x++ {
			var c0, c1 Color
			if premultiplied {
				c0 = nrgbaCode(pix[i], pix[i+1], pix[i+2], pix[i+3])
				c1 = nrgbaCode(pix[i+4], pix[i+5], pix[i+6], pix[i+7])
			} else {
				c0 = nearestCode(pix[i], pix[i+1], pix[i+2])
				c1 = nearestCode(pix[i+4], pix[i+5], pix[i+6])
			}
			out[x] = byte(c0)<<4 | byte(c1)
			i += 8
		}
	}
}

// nrgbaCode unpremultiplies one NRGBA pixel and returns its nearest
// panel code.
func nrgbaCode(r, g, b, a uint8) Color {
	if a == 0 {
		return Black
	}
	if a == 0xff {
		return nearestCode(r, g, b)
	}
	return nearestCode(uint8(int(r)*255/int(a)), uint8(int(g)*255/int(a)), uint8(int(b)*255/int(a)))
}