mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
## Summary **What is the goal of this PR?** Replaced manual 1-bit BMP header logic in `Xtc::generateCoverBmp()` and `Xtc::generateThumbBmp()` with calls to the existing `createBmpHeader()` utility. Added a `BmpRowOrder` enum and param to support the top-down row order of XTC cover images. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_
323 lines
9.0 KiB
C++
323 lines
9.0 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
struct BmpHeader;
|
|
|
|
// Helper functions
|
|
uint8_t quantize(int gray, int x, int y);
|
|
uint8_t quantizeSimple(int gray);
|
|
uint8_t quantize1bit(int gray, int x, int y);
|
|
int adjustPixel(int gray);
|
|
|
|
enum class BmpRowOrder { BottomUp, TopDown };
|
|
|
|
// Populates a 1-bit BMP header in the provided memory.
|
|
void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder);
|
|
|
|
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
|
|
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
|
|
// X 1/8 1/8
|
|
// 1/8 1/8 1/8
|
|
// 1/8
|
|
class Atkinson1BitDitherer {
|
|
public:
|
|
explicit Atkinson1BitDitherer(int width) : width(width) {
|
|
errorRow0 = new int16_t[width + 4](); // Current row
|
|
errorRow1 = new int16_t[width + 4](); // Next row
|
|
errorRow2 = new int16_t[width + 4](); // Row after next
|
|
}
|
|
|
|
~Atkinson1BitDitherer() {
|
|
delete[] errorRow0;
|
|
delete[] errorRow1;
|
|
delete[] errorRow2;
|
|
}
|
|
|
|
// EXPLICITLY DELETE THE COPY CONSTRUCTOR
|
|
Atkinson1BitDitherer(const Atkinson1BitDitherer& other) = delete;
|
|
|
|
// EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR
|
|
Atkinson1BitDitherer& operator=(const Atkinson1BitDitherer& other) = delete;
|
|
|
|
uint8_t processPixel(int gray, int x) {
|
|
// Apply brightness/contrast/gamma adjustments
|
|
gray = adjustPixel(gray);
|
|
|
|
// Add accumulated error
|
|
int adjusted = gray + errorRow0[x + 2];
|
|
if (adjusted < 0) adjusted = 0;
|
|
if (adjusted > 255) adjusted = 255;
|
|
|
|
// Quantize to 2 levels (1-bit): 0 = black, 1 = white
|
|
uint8_t quantized;
|
|
int quantizedValue;
|
|
if (adjusted < 128) {
|
|
quantized = 0;
|
|
quantizedValue = 0;
|
|
} else {
|
|
quantized = 1;
|
|
quantizedValue = 255;
|
|
}
|
|
|
|
// Calculate error (only distribute 6/8 = 75%)
|
|
int error = (adjusted - quantizedValue) >> 3; // error/8
|
|
|
|
// Distribute 1/8 to each of 6 neighbors
|
|
errorRow0[x + 3] += error; // Right
|
|
errorRow0[x + 4] += error; // Right+1
|
|
errorRow1[x + 1] += error; // Bottom-left
|
|
errorRow1[x + 2] += error; // Bottom
|
|
errorRow1[x + 3] += error; // Bottom-right
|
|
errorRow2[x + 2] += error; // Two rows down
|
|
|
|
return quantized;
|
|
}
|
|
|
|
void nextRow() {
|
|
int16_t* temp = errorRow0;
|
|
errorRow0 = errorRow1;
|
|
errorRow1 = errorRow2;
|
|
errorRow2 = temp;
|
|
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
|
|
}
|
|
|
|
void reset() {
|
|
memset(errorRow0, 0, (width + 4) * sizeof(int16_t));
|
|
memset(errorRow1, 0, (width + 4) * sizeof(int16_t));
|
|
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
|
|
}
|
|
|
|
private:
|
|
int width;
|
|
int16_t* errorRow0;
|
|
int16_t* errorRow1;
|
|
int16_t* errorRow2;
|
|
};
|
|
|
|
// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results
|
|
// Error distribution pattern:
|
|
// X 1/8 1/8
|
|
// 1/8 1/8 1/8
|
|
// 1/8
|
|
// Less error buildup = fewer artifacts than Floyd-Steinberg
|
|
class AtkinsonDitherer {
|
|
public:
|
|
explicit AtkinsonDitherer(int width) : width(width) {
|
|
errorRow0 = new int16_t[width + 4](); // Current row
|
|
errorRow1 = new int16_t[width + 4](); // Next row
|
|
errorRow2 = new int16_t[width + 4](); // Row after next
|
|
}
|
|
|
|
~AtkinsonDitherer() {
|
|
delete[] errorRow0;
|
|
delete[] errorRow1;
|
|
delete[] errorRow2;
|
|
}
|
|
// **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR**
|
|
AtkinsonDitherer(const AtkinsonDitherer& other) = delete;
|
|
|
|
// **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR**
|
|
AtkinsonDitherer& operator=(const AtkinsonDitherer& other) = delete;
|
|
|
|
uint8_t processPixel(int gray, int x) {
|
|
// Add accumulated error
|
|
int adjusted = gray + errorRow0[x + 2];
|
|
if (adjusted < 0) adjusted = 0;
|
|
if (adjusted > 255) adjusted = 255;
|
|
|
|
// Quantize to 4 levels
|
|
uint8_t quantized;
|
|
int quantizedValue;
|
|
if (false) { // original thresholds
|
|
if (adjusted < 43) {
|
|
quantized = 0;
|
|
quantizedValue = 0;
|
|
} else if (adjusted < 128) {
|
|
quantized = 1;
|
|
quantizedValue = 85;
|
|
} else if (adjusted < 213) {
|
|
quantized = 2;
|
|
quantizedValue = 170;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 255;
|
|
}
|
|
} else { // fine-tuned to X4 eink display
|
|
if (adjusted < 30) {
|
|
quantized = 0;
|
|
quantizedValue = 15;
|
|
} else if (adjusted < 50) {
|
|
quantized = 1;
|
|
quantizedValue = 30;
|
|
} else if (adjusted < 140) {
|
|
quantized = 2;
|
|
quantizedValue = 80;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 210;
|
|
}
|
|
}
|
|
|
|
// Calculate error (only distribute 6/8 = 75%)
|
|
int error = (adjusted - quantizedValue) >> 3; // error/8
|
|
|
|
// Distribute 1/8 to each of 6 neighbors
|
|
errorRow0[x + 3] += error; // Right
|
|
errorRow0[x + 4] += error; // Right+1
|
|
errorRow1[x + 1] += error; // Bottom-left
|
|
errorRow1[x + 2] += error; // Bottom
|
|
errorRow1[x + 3] += error; // Bottom-right
|
|
errorRow2[x + 2] += error; // Two rows down
|
|
|
|
return quantized;
|
|
}
|
|
|
|
void nextRow() {
|
|
int16_t* temp = errorRow0;
|
|
errorRow0 = errorRow1;
|
|
errorRow1 = errorRow2;
|
|
errorRow2 = temp;
|
|
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
|
|
}
|
|
|
|
void reset() {
|
|
memset(errorRow0, 0, (width + 4) * sizeof(int16_t));
|
|
memset(errorRow1, 0, (width + 4) * sizeof(int16_t));
|
|
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
|
|
}
|
|
|
|
private:
|
|
int width;
|
|
int16_t* errorRow0;
|
|
int16_t* errorRow1;
|
|
int16_t* errorRow2;
|
|
};
|
|
|
|
// Floyd-Steinberg error diffusion dithering with serpentine scanning
|
|
// Serpentine scanning alternates direction each row to reduce "worm" artifacts
|
|
// Error distribution pattern (left-to-right):
|
|
// X 7/16
|
|
// 3/16 5/16 1/16
|
|
// Error distribution pattern (right-to-left, mirrored):
|
|
// 1/16 5/16 3/16
|
|
// 7/16 X
|
|
class FloydSteinbergDitherer {
|
|
public:
|
|
explicit FloydSteinbergDitherer(int width) : width(width), rowCount(0) {
|
|
errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
|
|
errorNextRow = new int16_t[width + 2]();
|
|
}
|
|
|
|
~FloydSteinbergDitherer() {
|
|
delete[] errorCurRow;
|
|
delete[] errorNextRow;
|
|
}
|
|
|
|
// **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR**
|
|
FloydSteinbergDitherer(const FloydSteinbergDitherer& other) = delete;
|
|
|
|
// **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR**
|
|
FloydSteinbergDitherer& operator=(const FloydSteinbergDitherer& other) = delete;
|
|
|
|
// Process a single pixel and return quantized 2-bit value
|
|
// x is the logical x position (0 to width-1), direction handled internally
|
|
uint8_t processPixel(int gray, int x) {
|
|
// Add accumulated error to this pixel
|
|
int adjusted = gray + errorCurRow[x + 1];
|
|
|
|
// Clamp to valid range
|
|
if (adjusted < 0) adjusted = 0;
|
|
if (adjusted > 255) adjusted = 255;
|
|
|
|
// Quantize to 4 levels (0, 85, 170, 255)
|
|
uint8_t quantized;
|
|
int quantizedValue;
|
|
if (false) { // original thresholds
|
|
if (adjusted < 43) {
|
|
quantized = 0;
|
|
quantizedValue = 0;
|
|
} else if (adjusted < 128) {
|
|
quantized = 1;
|
|
quantizedValue = 85;
|
|
} else if (adjusted < 213) {
|
|
quantized = 2;
|
|
quantizedValue = 170;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 255;
|
|
}
|
|
} else { // fine-tuned to X4 eink display
|
|
if (adjusted < 30) {
|
|
quantized = 0;
|
|
quantizedValue = 15;
|
|
} else if (adjusted < 50) {
|
|
quantized = 1;
|
|
quantizedValue = 30;
|
|
} else if (adjusted < 140) {
|
|
quantized = 2;
|
|
quantizedValue = 80;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 210;
|
|
}
|
|
}
|
|
|
|
// Calculate error
|
|
int error = adjusted - quantizedValue;
|
|
|
|
// Distribute error to neighbors (serpentine: direction-aware)
|
|
if (!isReverseRow()) {
|
|
// Left to right: standard distribution
|
|
// Right: 7/16
|
|
errorCurRow[x + 2] += (error * 7) >> 4;
|
|
// Bottom-left: 3/16
|
|
errorNextRow[x] += (error * 3) >> 4;
|
|
// Bottom: 5/16
|
|
errorNextRow[x + 1] += (error * 5) >> 4;
|
|
// Bottom-right: 1/16
|
|
errorNextRow[x + 2] += (error) >> 4;
|
|
} else {
|
|
// Right to left: mirrored distribution
|
|
// Left: 7/16
|
|
errorCurRow[x] += (error * 7) >> 4;
|
|
// Bottom-right: 3/16
|
|
errorNextRow[x + 2] += (error * 3) >> 4;
|
|
// Bottom: 5/16
|
|
errorNextRow[x + 1] += (error * 5) >> 4;
|
|
// Bottom-left: 1/16
|
|
errorNextRow[x] += (error) >> 4;
|
|
}
|
|
|
|
return quantized;
|
|
}
|
|
|
|
// Call at the end of each row to swap buffers
|
|
void nextRow() {
|
|
// Swap buffers
|
|
int16_t* temp = errorCurRow;
|
|
errorCurRow = errorNextRow;
|
|
errorNextRow = temp;
|
|
// Clear the next row buffer
|
|
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
|
rowCount++;
|
|
}
|
|
|
|
// Check if current row should be processed in reverse
|
|
bool isReverseRow() const { return (rowCount & 1) != 0; }
|
|
|
|
// Reset for a new image or MCU block
|
|
void reset() {
|
|
memset(errorCurRow, 0, (width + 2) * sizeof(int16_t));
|
|
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
|
rowCount = 0;
|
|
}
|
|
|
|
private:
|
|
int width;
|
|
int rowCount;
|
|
int16_t* errorCurRow;
|
|
int16_t* errorNextRow;
|
|
};
|