fix: Prepare SD card font caches from txt reader (#1973)

## Summary

SD card font fixes:
- `TxtReaderActivity` needs to call `renderer.ensureSdCardFontReady` to
build the advance lookup table to support rendering with SD card fonts.
This revealed that `TxtReaderActivity` was inconsistently performing
layout with `getTextWidth`, when the renderer actually uses
`getTextAdvanceX`, which can lead to minor inconsistencies in alignment.
- Avoid allocating one big `allText` string in
`ParsedText::layoutAndExtractLines`. Instead, pass the vector of word
strings directly to `SdCardFont::buildAdvanceTable`, where the algorithm
just needs to iterate codepoints anyway.

---

### 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**_

---------

Co-authored-by: Justin Mitchell <justin@jmitch.com>
This commit is contained in:
Zach Nelson
2026-05-15 09:50:41 -05:00
committed by GitHub
co-authored by Justin Mitchell
parent 5fa5a71ba2
commit b186529120
6 changed files with 126 additions and 90 deletions
+16 -4
View File
@@ -192,6 +192,17 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
}
buffer[chunkSize] = '\0';
// Prime the SD card font's advance table with this chunk's codepoints.
// Without this, every getTextAdvanceX() call in the wrap loop below triggers
// on-demand glyph loads through the 8-slot overflow ring buffer, which
// thrashes for any text with more than 8 unique chars (i.e. all English),
// floods the heap with short-lived bitmap allocations, and eventually
// corrupts FreeRTOS state. The advance table persists across calls per
// font, so the cost amortizes to ~ASCII-size after the first chunk.
if (renderer.isSdCardFont(cachedFontId)) {
renderer.ensureSdCardFontReady(cachedFontId, reinterpret_cast<const char*>(buffer), /*styleMask=*/0x01);
}
// Parse lines from buffer
size_t pos = 0;
@@ -231,7 +242,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
break;
}
int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str());
int lineWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
if (lineWidth <= viewportWidth) {
outLines.push_back(line);
@@ -242,7 +253,8 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
// Find break point
size_t breakPos = line.length();
while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) {
while (breakPos > 0 && renderer.getTextAdvanceX(cachedFontId, line.substr(0, breakPos).c_str(),
EpdFontFamily::REGULAR) > viewportWidth) {
// Try to break at space
size_t spacePos = line.rfind(' ', breakPos - 1);
if (spacePos != std::string::npos && spacePos > 0) {
@@ -354,12 +366,12 @@ void TxtReaderActivity::renderPage() {
// x already set to left margin
break;
case CrossPointSettings::CENTER_ALIGN: {
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str());
int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
x = cachedOrientedMarginLeft + (contentWidth - textWidth) / 2;
break;
}
case CrossPointSettings::RIGHT_ALIGN: {
int textWidth = renderer.getTextWidth(cachedFontId, line.c_str());
int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
x = cachedOrientedMarginLeft + contentWidth - textWidth;
break;
}