324 lines
9.1 KiB
C
Raw Normal View History

#pragma once
#include <cstring>
// Global high contrast mode control
// When enabled, quantization thresholds are adjusted to push mid-grays toward black/white
void setHighContrastMode(bool enabled);
bool isHighContrastMode();
// Helper functions
uint8_t quantize(int gray, int x, int y);
uint8_t quantizeSimple(int gray);
Add cover image display in *Continue Reading* card with framebuffer caching (#200) ## Summary * **What is the goal of this PR?** (e.g., Fixes a bug in the user authentication module, Display the book cover image in the **"Continue Reading"** card on the home screen, with fast navigation using framebuffer caching. * **What changes are included?** - Display book cover image in the "Continue Reading" card on home screen - Load cover from cached BMP (same as sleep screen cover) - Add framebuffer store/restore functions (`copyStoredBwBuffer`, `freeStoredBwBuffer`) for fast navigation after initial render - Fix `drawBitmap` scaling bug: apply scale to offset only, not to base coordinates - Add white text boxes behind title/author/continue reading label for readability on cover - Support both EPUB and XTC file cover images - Increase HomeActivity task stack size from 2048 to 4096 for cover image rendering ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). - Performance: First render loads cover from SD card (~800ms), subsequent navigation uses cached framebuffer (~instant) - Memory: Framebuffer cache uses ~48KB (6 chunks × 8KB) while on home screen, freed on exit - Fallback: If cover image is not available, falls back to standard text-only display - The `drawBitmap` fix corrects a bug where screenY = (y + offset) scale was incorrectly scaling the base coordinates. Now correctly uses screenY = y + (offset scale)
2026-01-14 19:24:02 +09:00
uint8_t quantize1bit(int gray, int x, int y);
int adjustPixel(int gray);
Add cover image display in *Continue Reading* card with framebuffer caching (#200) ## Summary * **What is the goal of this PR?** (e.g., Fixes a bug in the user authentication module, Display the book cover image in the **"Continue Reading"** card on the home screen, with fast navigation using framebuffer caching. * **What changes are included?** - Display book cover image in the "Continue Reading" card on home screen - Load cover from cached BMP (same as sleep screen cover) - Add framebuffer store/restore functions (`copyStoredBwBuffer`, `freeStoredBwBuffer`) for fast navigation after initial render - Fix `drawBitmap` scaling bug: apply scale to offset only, not to base coordinates - Add white text boxes behind title/author/continue reading label for readability on cover - Support both EPUB and XTC file cover images - Increase HomeActivity task stack size from 2048 to 4096 for cover image rendering ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). - Performance: First render loads cover from SD card (~800ms), subsequent navigation uses cached framebuffer (~instant) - Memory: Framebuffer cache uses ~48KB (6 chunks × 8KB) while on home screen, freed on exit - Fallback: If cover image is not available, falls back to standard text-only display - The `drawBitmap` fix corrects a bug where screenY = (y + offset) scale was incorrectly scaling the base coordinates. Now correctly uses screenY = y + (offset scale)
2026-01-14 19:24:02 +09:00
// 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 (isHighContrastMode()) {
// High contrast thresholds - push mid-grays toward extremes
if (adjusted < 50) {
quantized = 0;
quantizedValue = 15;
} else if (adjusted < 90) {
quantized = 1;
quantizedValue = 50;
} else if (adjusted < 170) {
quantized = 2;
quantizedValue = 130;
} else {
quantized = 3;
quantizedValue = 230;
}
} else {
// Normal thresholds - 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
uint8_t quantized;
int quantizedValue;
if (isHighContrastMode()) {
// High contrast thresholds - push mid-grays toward extremes
if (adjusted < 50) {
quantized = 0;
quantizedValue = 15;
} else if (adjusted < 90) {
quantized = 1;
quantizedValue = 50;
} else if (adjusted < 170) {
quantized = 2;
quantizedValue = 130;
} else {
quantized = 3;
quantizedValue = 230;
}
} else {
// Normal thresholds - 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;
};