-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgrayscale.go
More file actions
127 lines (111 loc) · 3.43 KB
/
grayscale.go
File metadata and controls
127 lines (111 loc) · 3.43 KB
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
package xtx
import (
"image"
"image/color"
)
// Grayscale is a 2-bit per pixel, 4-level grayscale image corresponding to the
// XTH format. It implements [image.Image] and stores pixels in dual bit-plane
// format with vertical scan order (column-major, right-to-left) matching the
// Xteink e-paper display refresh pattern.
//
// The pixel value mapping follows the non-linear Xteink LUT:
//
// 0 = White, 1 = Dark Grey, 2 = Light Grey, 3 = Black
type Grayscale struct {
// Pix holds both bit planes concatenated: [plane1][plane2].
// Each plane uses vertical scan order: columns right-to-left,
// 8 vertical pixels packed per byte (MSB = topmost).
Pix []uint8
// Rect is the image bounds.
Rect image.Rectangle
// PlaneSize is the size of one bit plane in bytes.
PlaneSize int
// ColBytes is the number of bytes per column: (height + 7) / 8.
ColBytes int
}
// NewGrayscale creates a new [Grayscale] image with the given bounds.
func NewGrayscale(r image.Rectangle) *Grayscale {
w, h := r.Dx(), r.Dy()
colBytes := (h + 7) / 8
planeSize := w * colBytes
return &Grayscale{
Pix: make([]uint8, planeSize*2),
Rect: r,
PlaneSize: planeSize,
ColBytes: colBytes,
}
}
// ColorModel returns [Gray4Model].
func (img *Grayscale) ColorModel() color.Model {
return Gray4Model
}
// Bounds returns the image bounds.
func (img *Grayscale) Bounds() image.Rectangle {
return img.Rect
}
// At returns the color of the pixel at (x, y).
func (img *Grayscale) At(x, y int) color.Color {
if !(image.Point{x, y}.In(img.Rect)) {
return Gray4White
}
return img.Gray4At(x, y)
}
// Gray4At returns the [Gray4] color at (x, y) without bounds checking.
func (img *Grayscale) Gray4At(x, y int) Gray4 {
bit1, bit2 := img.getBits(x, y)
return Gray4{V: (bit1 << 1) | bit2}
}
// Set sets the pixel at (x, y) to color c, converting through [Gray4Model].
func (img *Grayscale) Set(x, y int, c color.Color) {
if !(image.Point{x, y}.In(img.Rect)) {
return
}
g := Gray4Model.Convert(c).(Gray4)
img.SetGray4(x, y, g)
}
// SetGray4 sets the pixel at (x, y) to the [Gray4] value without color conversion.
func (img *Grayscale) SetGray4(x, y int, c Gray4) {
if !(image.Point{x, y}.In(img.Rect)) {
return
}
bit1 := (c.V >> 1) & 1
bit2 := c.V & 1
img.setBits(x, y, bit1, bit2)
}
// DataSize returns the total size of the pixel data in bytes (both planes),
// matching the XTH spec: ((width * height + 7) / 8) * 2.
func (img *Grayscale) DataSize() uint32 {
return uint32(img.PlaneSize * 2)
}
// getBits reads the two bit-plane values for pixel (x, y).
// Vertical scan order: columns scan right-to-left, 8 vertical pixels per byte.
func (img *Grayscale) getBits(x, y int) (bit1, bit2 uint8) {
// Column index: right to left
col := img.Rect.Dx() - 1 - x
byteInCol := y / 8
bitInByte := 7 - uint(y%8) // MSB = topmost pixel in group
offset := col*img.ColBytes + byteInCol
bit1 = (img.Pix[offset] >> bitInByte) & 1
bit2 = (img.Pix[img.PlaneSize+offset] >> bitInByte) & 1
return
}
// setBits writes the two bit-plane values for pixel (x, y).
func (img *Grayscale) setBits(x, y int, bit1, bit2 uint8) {
col := img.Rect.Dx() - 1 - x
byteInCol := y / 8
bitInByte := 7 - uint(y%8)
offset := col*img.ColBytes + byteInCol
mask := uint8(1) << bitInByte
// Plane 1 (bit1)
if bit1 != 0 {
img.Pix[offset] |= mask
} else {
img.Pix[offset] &^= mask
}
// Plane 2 (bit2)
if bit2 != 0 {
img.Pix[img.PlaneSize+offset] |= mask
} else {
img.Pix[img.PlaneSize+offset] &^= mask
}
}