Address PR review feedback for TXT reader

- Add font ID to cache header for invalidation when font changes
- Use serialization module for consistent cache read/write
- Add screenMargin setting and read padding from settings
- Implement grayscale rendering pass for anti-aliased fonts
- Add text alignment support (left, center, right, justified)
- Bump cache version to invalidate old caches
This commit is contained in:
Eunchurn Park 2026-01-09 23:10:24 +09:00
parent 2072741a59
commit f885efeeb9
No known key found for this signature in database
GPG Key ID: 29D94D9C697E3F92
4 changed files with 180 additions and 91 deletions

View File

@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 13; constexpr uint8_t SETTINGS_COUNT = 14;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -40,6 +40,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, paragraphAlignment); serialization::writePod(outputFile, paragraphAlignment);
serialization::writePod(outputFile, sleepTimeout); serialization::writePod(outputFile, sleepTimeout);
serialization::writePod(outputFile, refreshFrequency); serialization::writePod(outputFile, refreshFrequency);
serialization::writePod(outputFile, screenMargin);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -92,6 +93,8 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, refreshFrequency); serialization::readPod(inputFile, refreshFrequency);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, screenMargin);
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();
@ -167,6 +170,22 @@ int CrossPointSettings::getRefreshFrequency() const {
} }
} }
int CrossPointSettings::getScreenMargin() const {
switch (screenMargin) {
case MARGIN_0:
return 0;
case MARGIN_5:
default:
return 5;
case MARGIN_10:
return 10;
case MARGIN_15:
return 15;
case MARGIN_20:
return 20;
}
}
int CrossPointSettings::getReaderFontId() const { int CrossPointSettings::getReaderFontId() const {
switch (fontFamily) { switch (fontFamily) {
case BOOKERLY: case BOOKERLY:

View File

@ -51,6 +51,9 @@ class CrossPointSettings {
// E-ink refresh frequency (pages between full refreshes) // E-ink refresh frequency (pages between full refreshes)
enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 }; enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 };
// Screen margin options
enum SCREEN_MARGIN { MARGIN_0 = 0, MARGIN_5 = 1, MARGIN_10 = 2, MARGIN_15 = 3, MARGIN_20 = 4 };
// Sleep screen settings // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t sleepScreen = DARK;
// Status bar settings // Status bar settings
@ -74,6 +77,8 @@ class CrossPointSettings {
uint8_t sleepTimeout = SLEEP_10_MIN; uint8_t sleepTimeout = SLEEP_10_MIN;
// E-ink refresh frequency (default 15 pages) // E-ink refresh frequency (default 15 pages)
uint8_t refreshFrequency = REFRESH_15; uint8_t refreshFrequency = REFRESH_15;
// Screen margin setting (default 5px)
uint8_t screenMargin = MARGIN_5;
~CrossPointSettings() = default; ~CrossPointSettings() = default;
@ -89,6 +94,7 @@ class CrossPointSettings {
float getReaderLineCompression() const; float getReaderLineCompression() const;
unsigned long getSleepTimeoutMs() const; unsigned long getSleepTimeoutMs() const;
int getRefreshFrequency() const; int getRefreshFrequency() const;
int getScreenMargin() const;
}; };
// Helper macro to access settings // Helper macro to access settings

View File

@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <Serialization.h>
#include <Utf8.h> #include <Utf8.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@ -12,10 +13,12 @@
namespace { namespace {
constexpr unsigned long goHomeMs = 1000; constexpr unsigned long goHomeMs = 1000;
constexpr int topPadding = 10;
constexpr int horizontalPadding = 15;
constexpr int statusBarMargin = 25; constexpr int statusBarMargin = 25;
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
// Cache file magic and version
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
} // namespace } // namespace
void TxtReaderActivity::taskTrampoline(void* param) { void TxtReaderActivity::taskTrampoline(void* param) {
@ -139,18 +142,23 @@ void TxtReaderActivity::initializeReader() {
return; return;
} }
// Store current settings for cache validation
cachedFontId = SETTINGS.getReaderFontId();
cachedScreenMargin = SETTINGS.getScreenMargin();
cachedParagraphAlignment = SETTINGS.paragraphAlignment;
// Calculate viewport dimensions // Calculate viewport dimensions
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
&orientedMarginLeft); &orientedMarginLeft);
orientedMarginTop += topPadding; orientedMarginTop += cachedScreenMargin;
orientedMarginLeft += horizontalPadding; orientedMarginLeft += cachedScreenMargin;
orientedMarginRight += horizontalPadding; orientedMarginRight += cachedScreenMargin;
orientedMarginBottom += statusBarMargin; orientedMarginBottom += statusBarMargin;
viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
const int viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; const int viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
const int lineHeight = renderer.getLineHeight(SETTINGS.getReaderFontId()); const int lineHeight = renderer.getLineHeight(cachedFontId);
linesPerPage = viewportHeight / lineHeight; linesPerPage = viewportHeight / lineHeight;
if (linesPerPage < 1) linesPerPage = 1; if (linesPerPage < 1) linesPerPage = 1;
@ -291,7 +299,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
// Word wrap if needed // Word wrap if needed
while (!line.empty() && static_cast<int>(outLines.size()) < linesPerPage) { while (!line.empty() && static_cast<int>(outLines.size()) < linesPerPage) {
int lineWidth = renderer.getTextWidth(SETTINGS.getReaderFontId(), line.c_str()); int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str());
if (lineWidth <= viewportWidth) { if (lineWidth <= viewportWidth) {
outLines.push_back(line); outLines.push_back(line);
@ -301,7 +309,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
// Find break point // Find break point
size_t breakPos = line.length(); size_t breakPos = line.length();
while (breakPos > 0 && while (breakPos > 0 &&
renderer.getTextWidth(SETTINGS.getReaderFontId(), line.substr(0, breakPos).c_str()) > viewportWidth) { renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) {
// Try to break at space // Try to break at space
size_t spacePos = line.rfind(' ', breakPos - 1); size_t spacePos = line.rfind(' ', breakPos - 1);
if (spacePos != std::string::npos && spacePos > 0) { if (spacePos != std::string::npos && spacePos > 0) {
@ -393,21 +401,51 @@ void TxtReaderActivity::renderPage() {
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
&orientedMarginLeft); &orientedMarginLeft);
orientedMarginTop += topPadding; orientedMarginTop += cachedScreenMargin;
orientedMarginLeft += horizontalPadding; orientedMarginLeft += cachedScreenMargin;
orientedMarginRight += horizontalPadding; orientedMarginRight += cachedScreenMargin;
orientedMarginBottom += statusBarMargin; orientedMarginBottom += statusBarMargin;
const int lineHeight = renderer.getLineHeight(SETTINGS.getReaderFontId()); const int lineHeight = renderer.getLineHeight(cachedFontId);
const int contentWidth = viewportWidth;
int y = orientedMarginTop; // Render text lines with alignment
for (const auto& line : currentPageLines) { auto renderLines = [&]() {
if (!line.empty()) { int y = orientedMarginTop;
renderer.drawText(SETTINGS.getReaderFontId(), orientedMarginLeft, y, line.c_str()); for (const auto& line : currentPageLines) {
if (!line.empty()) {
int x = orientedMarginLeft;
// Apply text alignment
switch (cachedParagraphAlignment) {
case CrossPointSettings::LEFT_ALIGN:
default:
// x already set to left margin
break;
case CrossPointSettings::CENTER_ALIGN: {
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str());
x = orientedMarginLeft + (contentWidth - textWidth) / 2;
break;
}
case CrossPointSettings::RIGHT_ALIGN: {
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str());
x = orientedMarginLeft + contentWidth - textWidth;
break;
}
case CrossPointSettings::JUSTIFIED:
// For plain text, justified is treated as left-aligned
// (true justification would require word spacing adjustments)
break;
}
renderer.drawText(cachedFontId, x, y, line.c_str());
}
y += lineHeight;
} }
y += lineHeight; };
}
// First pass: BW rendering
renderLines();
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
@ -417,6 +455,28 @@ void TxtReaderActivity::renderPage() {
renderer.displayBuffer(); renderer.displayBuffer();
pagesUntilFullRefresh--; pagesUntilFullRefresh--;
} }
// Save BW buffer for restoration after grayscale pass
renderer.storeBwBuffer();
// Grayscale rendering pass (for anti-aliased fonts)
{
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
renderLines();
renderer.copyGrayscaleLsbBuffers();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
renderLines();
renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer();
renderer.setRenderMode(GfxRenderer::BW);
}
// Restore BW buffer
renderer.restoreBwBuffer();
} }
void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom,
@ -492,13 +552,17 @@ void TxtReaderActivity::loadProgress() {
} }
bool TxtReaderActivity::loadPageIndexCache() { bool TxtReaderActivity::loadPageIndexCache() {
// Cache file format: // Cache file format (using serialization module):
// - 4 bytes: magic "TXTI" // - uint32_t: magic "TXTI"
// - 4 bytes: file size (to validate cache) // - uint8_t: cache version
// - 4 bytes: viewport width // - uint32_t: file size (to validate cache)
// - 4 bytes: lines per page // - int32_t: viewport width
// - 4 bytes: total pages count // - int32_t: lines per page
// - N * 4 bytes: page offsets (size_t stored as uint32_t) // - int32_t: font ID (to invalidate cache on font change)
// - int32_t: screen margin (to invalidate cache on margin change)
// - uint8_t: paragraph alignment (to invalidate cache on alignment change)
// - uint32_t: total pages count
// - N * uint32_t: page offsets
std::string cachePath = txt->getCachePath() + "/index.bin"; std::string cachePath = txt->getCachePath() + "/index.bin";
FsFile f; FsFile f;
@ -507,58 +571,81 @@ bool TxtReaderActivity::loadPageIndexCache() {
return false; return false;
} }
// Read and validate header // Read and validate header using serialization module
uint8_t header[20]; uint32_t magic;
if (f.read(header, 20) != 20) { serialization::readPod(f, magic);
if (magic != CACHE_MAGIC) {
Serial.printf("[%lu] [TRS] Cache magic mismatch, rebuilding\n", millis());
f.close(); f.close();
return false; return false;
} }
// Check magic uint8_t version;
if (header[0] != 'T' || header[1] != 'X' || header[2] != 'T' || header[3] != 'I') { serialization::readPod(f, version);
if (version != CACHE_VERSION) {
Serial.printf("[%lu] [TRS] Cache version mismatch (%d != %d), rebuilding\n", millis(), version, CACHE_VERSION);
f.close(); f.close();
return false; return false;
} }
// Check file size matches uint32_t fileSize;
uint32_t cachedFileSize = header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24); serialization::readPod(f, fileSize);
if (cachedFileSize != txt->getFileSize()) { if (fileSize != txt->getFileSize()) {
Serial.printf("[%lu] [TRS] Cache file size mismatch, rebuilding\n", millis()); Serial.printf("[%lu] [TRS] Cache file size mismatch, rebuilding\n", millis());
f.close(); f.close();
return false; return false;
} }
// Check viewport width matches int32_t cachedWidth;
uint32_t cachedViewportWidth = header[8] | (header[9] << 8) | (header[10] << 16) | (header[11] << 24); serialization::readPod(f, cachedWidth);
if (static_cast<int>(cachedViewportWidth) != viewportWidth) { if (cachedWidth != viewportWidth) {
Serial.printf("[%lu] [TRS] Cache viewport width mismatch, rebuilding\n", millis()); Serial.printf("[%lu] [TRS] Cache viewport width mismatch, rebuilding\n", millis());
f.close(); f.close();
return false; return false;
} }
// Check lines per page matches int32_t cachedLines;
uint32_t cachedLinesPerPage = header[12] | (header[13] << 8) | (header[14] << 16) | (header[15] << 24); serialization::readPod(f, cachedLines);
if (static_cast<int>(cachedLinesPerPage) != linesPerPage) { if (cachedLines != linesPerPage) {
Serial.printf("[%lu] [TRS] Cache lines per page mismatch, rebuilding\n", millis()); Serial.printf("[%lu] [TRS] Cache lines per page mismatch, rebuilding\n", millis());
f.close(); f.close();
return false; return false;
} }
// Read total pages int32_t fontId;
uint32_t cachedTotalPages = header[16] | (header[17] << 8) | (header[18] << 16) | (header[19] << 24); serialization::readPod(f, fontId);
if (fontId != cachedFontId) {
Serial.printf("[%lu] [TRS] Cache font ID mismatch (%d != %d), rebuilding\n", millis(), fontId, cachedFontId);
f.close();
return false;
}
int32_t margin;
serialization::readPod(f, margin);
if (margin != cachedScreenMargin) {
Serial.printf("[%lu] [TRS] Cache screen margin mismatch, rebuilding\n", millis());
f.close();
return false;
}
uint8_t alignment;
serialization::readPod(f, alignment);
if (alignment != cachedParagraphAlignment) {
Serial.printf("[%lu] [TRS] Cache paragraph alignment mismatch, rebuilding\n", millis());
f.close();
return false;
}
uint32_t numPages;
serialization::readPod(f, numPages);
// Read page offsets // Read page offsets
pageOffsets.clear(); pageOffsets.clear();
pageOffsets.reserve(cachedTotalPages); pageOffsets.reserve(numPages);
for (uint32_t i = 0; i < cachedTotalPages; i++) { for (uint32_t i = 0; i < numPages; i++) {
uint8_t offsetData[4]; uint32_t offset;
if (f.read(offsetData, 4) != 4) { serialization::readPod(f, offset);
f.close();
pageOffsets.clear();
return false;
}
uint32_t offset = offsetData[0] | (offsetData[1] << 8) | (offsetData[2] << 16) | (offsetData[3] << 24);
pageOffsets.push_back(offset); pageOffsets.push_back(offset);
} }
@ -576,49 +663,20 @@ void TxtReaderActivity::savePageIndexCache() const {
return; return;
} }
// Write header // Write header using serialization module
uint8_t header[20]; serialization::writePod(f, CACHE_MAGIC);
header[0] = 'T'; serialization::writePod(f, CACHE_VERSION);
header[1] = 'X'; serialization::writePod(f, static_cast<uint32_t>(txt->getFileSize()));
header[2] = 'T'; serialization::writePod(f, static_cast<int32_t>(viewportWidth));
header[3] = 'I'; serialization::writePod(f, static_cast<int32_t>(linesPerPage));
serialization::writePod(f, static_cast<int32_t>(cachedFontId));
// File size serialization::writePod(f, static_cast<int32_t>(cachedScreenMargin));
uint32_t fileSize = txt->getFileSize(); serialization::writePod(f, cachedParagraphAlignment);
header[4] = fileSize & 0xFF; serialization::writePod(f, static_cast<uint32_t>(pageOffsets.size()));
header[5] = (fileSize >> 8) & 0xFF;
header[6] = (fileSize >> 16) & 0xFF;
header[7] = (fileSize >> 24) & 0xFF;
// Viewport width
header[8] = viewportWidth & 0xFF;
header[9] = (viewportWidth >> 8) & 0xFF;
header[10] = (viewportWidth >> 16) & 0xFF;
header[11] = (viewportWidth >> 24) & 0xFF;
// Lines per page
header[12] = linesPerPage & 0xFF;
header[13] = (linesPerPage >> 8) & 0xFF;
header[14] = (linesPerPage >> 16) & 0xFF;
header[15] = (linesPerPage >> 24) & 0xFF;
// Total pages
uint32_t numPages = pageOffsets.size();
header[16] = numPages & 0xFF;
header[17] = (numPages >> 8) & 0xFF;
header[18] = (numPages >> 16) & 0xFF;
header[19] = (numPages >> 24) & 0xFF;
f.write(header, 20);
// Write page offsets // Write page offsets
for (size_t offset : pageOffsets) { for (size_t offset : pageOffsets) {
uint8_t offsetData[4]; serialization::writePod(f, static_cast<uint32_t>(offset));
offsetData[0] = offset & 0xFF;
offsetData[1] = (offset >> 8) & 0xFF;
offsetData[2] = (offset >> 16) & 0xFF;
offsetData[3] = (offset >> 24) & 0xFF;
f.write(offsetData, 4);
} }
f.close(); f.close();

View File

@ -7,6 +7,7 @@
#include <vector> #include <vector>
#include "CrossPointSettings.h"
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
class TxtReaderActivity final : public ActivityWithSubactivity { class TxtReaderActivity final : public ActivityWithSubactivity {
@ -27,6 +28,11 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
int viewportWidth = 0; int viewportWidth = 0;
bool initialized = false; bool initialized = false;
// Cached settings for cache validation (different fonts/margins require re-indexing)
int cachedFontId = 0;
int cachedScreenMargin = 0;
uint8_t cachedParagraphAlignment = CrossPointSettings::LEFT_ALIGN;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void renderScreen(); void renderScreen();