A fork of https://github.com/crosspoint-reader/crosspoint-reader
1#pragma once
2
3#include <cstdint>
4#include <cstring>
5
6struct BmpHeader;
7
8// Helper functions
9uint8_t quantize(int gray, int x, int y);
10uint8_t quantizeSimple(int gray);
11uint8_t quantize1bit(int gray, int x, int y);
12int adjustPixel(int gray);
13
14enum class BmpRowOrder { BottomUp, TopDown };
15
16// Populates a 1-bit BMP header in the provided memory.
17void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder);
18
19// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
20// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
21// X 1/8 1/8
22// 1/8 1/8 1/8
23// 1/8
24class Atkinson1BitDitherer {
25 public:
26 explicit Atkinson1BitDitherer(int width) : width(width) {
27 errorRow0 = new int16_t[width + 4](); // Current row
28 errorRow1 = new int16_t[width + 4](); // Next row
29 errorRow2 = new int16_t[width + 4](); // Row after next
30 }
31
32 ~Atkinson1BitDitherer() {
33 delete[] errorRow0;
34 delete[] errorRow1;
35 delete[] errorRow2;
36 }
37
38 // EXPLICITLY DELETE THE COPY CONSTRUCTOR
39 Atkinson1BitDitherer(const Atkinson1BitDitherer& other) = delete;
40
41 // EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR
42 Atkinson1BitDitherer& operator=(const Atkinson1BitDitherer& other) = delete;
43
44 uint8_t processPixel(int gray, int x) {
45 // Apply brightness/contrast/gamma adjustments
46 gray = adjustPixel(gray);
47
48 // Add accumulated error
49 int adjusted = gray + errorRow0[x + 2];
50 if (adjusted < 0) adjusted = 0;
51 if (adjusted > 255) adjusted = 255;
52
53 // Quantize to 2 levels (1-bit): 0 = black, 1 = white
54 uint8_t quantized;
55 int quantizedValue;
56 if (adjusted < 128) {
57 quantized = 0;
58 quantizedValue = 0;
59 } else {
60 quantized = 1;
61 quantizedValue = 255;
62 }
63
64 // Calculate error (only distribute 6/8 = 75%)
65 int error = (adjusted - quantizedValue) >> 3; // error/8
66
67 // Distribute 1/8 to each of 6 neighbors
68 errorRow0[x + 3] += error; // Right
69 errorRow0[x + 4] += error; // Right+1
70 errorRow1[x + 1] += error; // Bottom-left
71 errorRow1[x + 2] += error; // Bottom
72 errorRow1[x + 3] += error; // Bottom-right
73 errorRow2[x + 2] += error; // Two rows down
74
75 return quantized;
76 }
77
78 void nextRow() {
79 int16_t* temp = errorRow0;
80 errorRow0 = errorRow1;
81 errorRow1 = errorRow2;
82 errorRow2 = temp;
83 memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
84 }
85
86 void reset() {
87 memset(errorRow0, 0, (width + 4) * sizeof(int16_t));
88 memset(errorRow1, 0, (width + 4) * sizeof(int16_t));
89 memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
90 }
91
92 private:
93 int width;
94 int16_t* errorRow0;
95 int16_t* errorRow1;
96 int16_t* errorRow2;
97};
98
99// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results
100// Error distribution pattern:
101// X 1/8 1/8
102// 1/8 1/8 1/8
103// 1/8
104// Less error buildup = fewer artifacts than Floyd-Steinberg
105class AtkinsonDitherer {
106 public:
107 explicit AtkinsonDitherer(int width) : width(width) {
108 errorRow0 = new int16_t[width + 4](); // Current row
109 errorRow1 = new int16_t[width + 4](); // Next row
110 errorRow2 = new int16_t[width + 4](); // Row after next
111 }
112
113 ~AtkinsonDitherer() {
114 delete[] errorRow0;
115 delete[] errorRow1;
116 delete[] errorRow2;
117 }
118 // **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR**
119 AtkinsonDitherer(const AtkinsonDitherer& other) = delete;
120
121 // **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR**
122 AtkinsonDitherer& operator=(const AtkinsonDitherer& other) = delete;
123
124 uint8_t processPixel(int gray, int x) {
125 // Add accumulated error
126 int adjusted = gray + errorRow0[x + 2];
127 if (adjusted < 0) adjusted = 0;
128 if (adjusted > 255) adjusted = 255;
129
130 // Quantize to 4 levels
131 uint8_t quantized;
132 int quantizedValue;
133 if (false) { // original thresholds
134 if (adjusted < 43) {
135 quantized = 0;
136 quantizedValue = 0;
137 } else if (adjusted < 128) {
138 quantized = 1;
139 quantizedValue = 85;
140 } else if (adjusted < 213) {
141 quantized = 2;
142 quantizedValue = 170;
143 } else {
144 quantized = 3;
145 quantizedValue = 255;
146 }
147 } else { // fine-tuned to X4 eink display
148 if (adjusted < 30) {
149 quantized = 0;
150 quantizedValue = 15;
151 } else if (adjusted < 50) {
152 quantized = 1;
153 quantizedValue = 30;
154 } else if (adjusted < 140) {
155 quantized = 2;
156 quantizedValue = 80;
157 } else {
158 quantized = 3;
159 quantizedValue = 210;
160 }
161 }
162
163 // Calculate error (only distribute 6/8 = 75%)
164 int error = (adjusted - quantizedValue) >> 3; // error/8
165
166 // Distribute 1/8 to each of 6 neighbors
167 errorRow0[x + 3] += error; // Right
168 errorRow0[x + 4] += error; // Right+1
169 errorRow1[x + 1] += error; // Bottom-left
170 errorRow1[x + 2] += error; // Bottom
171 errorRow1[x + 3] += error; // Bottom-right
172 errorRow2[x + 2] += error; // Two rows down
173
174 return quantized;
175 }
176
177 void nextRow() {
178 int16_t* temp = errorRow0;
179 errorRow0 = errorRow1;
180 errorRow1 = errorRow2;
181 errorRow2 = temp;
182 memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
183 }
184
185 void reset() {
186 memset(errorRow0, 0, (width + 4) * sizeof(int16_t));
187 memset(errorRow1, 0, (width + 4) * sizeof(int16_t));
188 memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
189 }
190
191 private:
192 int width;
193 int16_t* errorRow0;
194 int16_t* errorRow1;
195 int16_t* errorRow2;
196};
197
198// Floyd-Steinberg error diffusion dithering with serpentine scanning
199// Serpentine scanning alternates direction each row to reduce "worm" artifacts
200// Error distribution pattern (left-to-right):
201// X 7/16
202// 3/16 5/16 1/16
203// Error distribution pattern (right-to-left, mirrored):
204// 1/16 5/16 3/16
205// 7/16 X
206class FloydSteinbergDitherer {
207 public:
208 explicit FloydSteinbergDitherer(int width) : width(width), rowCount(0) {
209 errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
210 errorNextRow = new int16_t[width + 2]();
211 }
212
213 ~FloydSteinbergDitherer() {
214 delete[] errorCurRow;
215 delete[] errorNextRow;
216 }
217
218 // **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR**
219 FloydSteinbergDitherer(const FloydSteinbergDitherer& other) = delete;
220
221 // **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR**
222 FloydSteinbergDitherer& operator=(const FloydSteinbergDitherer& other) = delete;
223
224 // Process a single pixel and return quantized 2-bit value
225 // x is the logical x position (0 to width-1), direction handled internally
226 uint8_t processPixel(int gray, int x) {
227 // Add accumulated error to this pixel
228 int adjusted = gray + errorCurRow[x + 1];
229
230 // Clamp to valid range
231 if (adjusted < 0) adjusted = 0;
232 if (adjusted > 255) adjusted = 255;
233
234 // Quantize to 4 levels (0, 85, 170, 255)
235 uint8_t quantized;
236 int quantizedValue;
237 if (false) { // original thresholds
238 if (adjusted < 43) {
239 quantized = 0;
240 quantizedValue = 0;
241 } else if (adjusted < 128) {
242 quantized = 1;
243 quantizedValue = 85;
244 } else if (adjusted < 213) {
245 quantized = 2;
246 quantizedValue = 170;
247 } else {
248 quantized = 3;
249 quantizedValue = 255;
250 }
251 } else { // fine-tuned to X4 eink display
252 if (adjusted < 30) {
253 quantized = 0;
254 quantizedValue = 15;
255 } else if (adjusted < 50) {
256 quantized = 1;
257 quantizedValue = 30;
258 } else if (adjusted < 140) {
259 quantized = 2;
260 quantizedValue = 80;
261 } else {
262 quantized = 3;
263 quantizedValue = 210;
264 }
265 }
266
267 // Calculate error
268 int error = adjusted - quantizedValue;
269
270 // Distribute error to neighbors (serpentine: direction-aware)
271 if (!isReverseRow()) {
272 // Left to right: standard distribution
273 // Right: 7/16
274 errorCurRow[x + 2] += (error * 7) >> 4;
275 // Bottom-left: 3/16
276 errorNextRow[x] += (error * 3) >> 4;
277 // Bottom: 5/16
278 errorNextRow[x + 1] += (error * 5) >> 4;
279 // Bottom-right: 1/16
280 errorNextRow[x + 2] += (error) >> 4;
281 } else {
282 // Right to left: mirrored distribution
283 // Left: 7/16
284 errorCurRow[x] += (error * 7) >> 4;
285 // Bottom-right: 3/16
286 errorNextRow[x + 2] += (error * 3) >> 4;
287 // Bottom: 5/16
288 errorNextRow[x + 1] += (error * 5) >> 4;
289 // Bottom-left: 1/16
290 errorNextRow[x] += (error) >> 4;
291 }
292
293 return quantized;
294 }
295
296 // Call at the end of each row to swap buffers
297 void nextRow() {
298 // Swap buffers
299 int16_t* temp = errorCurRow;
300 errorCurRow = errorNextRow;
301 errorNextRow = temp;
302 // Clear the next row buffer
303 memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
304 rowCount++;
305 }
306
307 // Check if current row should be processed in reverse
308 bool isReverseRow() const { return (rowCount & 1) != 0; }
309
310 // Reset for a new image or MCU block
311 void reset() {
312 memset(errorCurRow, 0, (width + 2) * sizeof(int16_t));
313 memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
314 rowCount = 0;
315 }
316
317 private:
318 int width;
319 int rowCount;
320 int16_t* errorCurRow;
321 int16_t* errorNextRow;
322};