2026-01-12 12:36:19 +01:00
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-01-12 12:36:19 +01:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-12 12:36:19 +01:00
|
|
|
// 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;
|
|
|
|
|
};
|