A fork of https://github.com/crosspoint-reader/crosspoint-reader
1#include "BitmapHelpers.h"
2
3#include <cstdint>
4#include <cstring> // Added for memset
5
6#include "Bitmap.h"
7
8// Brightness/Contrast adjustments:
9constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments
10constexpr int BRIGHTNESS_BOOST = 10; // Brightness offset (0-50)
11constexpr bool GAMMA_CORRECTION = false; // Gamma curve (brightens midtones)
12constexpr float CONTRAST_FACTOR = 1.15f; // Contrast multiplier (1.0 = no change, >1 = more contrast)
13constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering
14
15// Integer approximation of gamma correction (brightens midtones)
16// Uses a simple curve: out = 255 * sqrt(in/255) ≈ sqrt(in * 255)
17static inline int applyGamma(int gray) {
18 if (!GAMMA_CORRECTION) return gray;
19 // Fast integer square root approximation for gamma ~0.5 (brightening)
20 // This brightens dark/mid tones while preserving highlights
21 const int product = gray * 255;
22 // Newton-Raphson integer sqrt (2 iterations for good accuracy)
23 int x = gray;
24 if (x > 0) {
25 x = (x + product / x) >> 1;
26 x = (x + product / x) >> 1;
27 }
28 return x > 255 ? 255 : x;
29}
30
31// Apply contrast adjustment around midpoint (128)
32// factor > 1.0 increases contrast, < 1.0 decreases
33static inline int applyContrast(int gray) {
34 // Integer-based contrast: (gray - 128) * factor + 128
35 // Using fixed-point: factor 1.15 ≈ 115/100
36 constexpr int factorNum = static_cast<int>(CONTRAST_FACTOR * 100);
37 int adjusted = ((gray - 128) * factorNum) / 100 + 128;
38 if (adjusted < 0) adjusted = 0;
39 if (adjusted > 255) adjusted = 255;
40 return adjusted;
41}
42// Combined brightness/contrast/gamma adjustment
43int adjustPixel(int gray) {
44 if (!USE_BRIGHTNESS) return gray;
45
46 // Order: contrast first, then brightness, then gamma
47 gray = applyContrast(gray);
48 gray += BRIGHTNESS_BOOST;
49 if (gray > 255) gray = 255;
50 if (gray < 0) gray = 0;
51 gray = applyGamma(gray);
52
53 return gray;
54}
55// Simple quantization without dithering - divide into 4 levels
56// The thresholds are fine-tuned to the X4 display
57uint8_t quantizeSimple(int gray) {
58 if (gray < 45) {
59 return 0;
60 } else if (gray < 70) {
61 return 1;
62 } else if (gray < 140) {
63 return 2;
64 } else {
65 return 3;
66 }
67}
68
69// Hash-based noise dithering - survives downsampling without moiré artifacts
70// Uses integer hash to generate pseudo-random threshold per pixel
71static inline uint8_t quantizeNoise(int gray, int x, int y) {
72 uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
73 hash = (hash ^ (hash >> 13)) * 1274126177u;
74 const int threshold = static_cast<int>(hash >> 24);
75
76 const int scaled = gray * 3;
77 if (scaled < 255) {
78 return (scaled + threshold >= 255) ? 1 : 0;
79 } else if (scaled < 510) {
80 return ((scaled - 255) + threshold >= 255) ? 2 : 1;
81 } else {
82 return ((scaled - 510) + threshold >= 255) ? 3 : 2;
83 }
84}
85
86// Main quantization function - selects between methods based on config
87uint8_t quantize(int gray, int x, int y) {
88 if (USE_NOISE_DITHERING) {
89 return quantizeNoise(gray, x, y);
90 } else {
91 return quantizeSimple(gray);
92 }
93}
94
95// 1-bit noise dithering for fast home screen rendering
96// Uses hash-based noise for consistent dithering that works well at small sizes
97uint8_t quantize1bit(int gray, int x, int y) {
98 gray = adjustPixel(gray);
99
100 // Generate noise threshold using integer hash (no regular pattern to alias)
101 uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
102 hash = (hash ^ (hash >> 13)) * 1274126177u;
103 const int threshold = static_cast<int>(hash >> 24); // 0-255
104
105 // Simple threshold with noise: gray >= (128 + noise offset) -> white
106 // The noise adds variation around the 128 midpoint
107 const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
108 return (gray >= adjustedThreshold) ? 1 : 0;
109}
110
111void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder) {
112 if (!bmpHeader) return;
113
114 // Zero out the memory to ensure no garbage data if called on uninitialized stack memory
115 std::memset(bmpHeader, 0, sizeof(BmpHeader));
116
117 uint32_t rowSize = (width + 31) / 32 * 4;
118 uint32_t imageSize = rowSize * height;
119 uint32_t fileSize = sizeof(BmpHeader) + imageSize;
120
121 bmpHeader->fileHeader.bfType = 0x4D42;
122 bmpHeader->fileHeader.bfSize = fileSize;
123 bmpHeader->fileHeader.bfReserved1 = 0;
124 bmpHeader->fileHeader.bfReserved2 = 0;
125 bmpHeader->fileHeader.bfOffBits = sizeof(BmpHeader);
126
127 bmpHeader->infoHeader.biSize = sizeof(bmpHeader->infoHeader);
128 bmpHeader->infoHeader.biWidth = width;
129 bmpHeader->infoHeader.biHeight = (rowOrder == BmpRowOrder::TopDown) ? -height : height;
130 bmpHeader->infoHeader.biPlanes = 1;
131 bmpHeader->infoHeader.biBitCount = 1;
132 bmpHeader->infoHeader.biCompression = 0;
133 bmpHeader->infoHeader.biSizeImage = imageSize;
134 bmpHeader->infoHeader.biXPelsPerMeter = 2835; // 72 DPI
135 bmpHeader->infoHeader.biYPelsPerMeter = 2835; // 72 DPI
136 bmpHeader->infoHeader.biClrUsed = 2;
137 bmpHeader->infoHeader.biClrImportant = 2;
138
139 // Color 0 (black)
140 bmpHeader->colors[0].rgbBlue = 0;
141 bmpHeader->colors[0].rgbGreen = 0;
142 bmpHeader->colors[0].rgbRed = 0;
143 bmpHeader->colors[0].rgbReserved = 0;
144
145 // Color 1 (white)
146 bmpHeader->colors[1].rgbBlue = 255;
147 bmpHeader->colors[1].rgbGreen = 255;
148 bmpHeader->colors[1].rgbRed = 255;
149 bmpHeader->colors[1].rgbReserved = 0;
150}