Compare commits

...

4 Commits

Author SHA1 Message Date
Eunchurn Park
8e4ce5225c
perf(ui): skip progress bar for chapters under 50KB
Always show "Indexing..." text, only add progress bar for >= 50KB chapters.
2025-12-27 16:25:26 +09:00
Eunchurn Park
11a059f39a
chore(fix): clang-format fix 2025-12-27 15:46:22 +09:00
Eunchurn Park
d3848779f9
fix(parser): handle oversized words in line break algorithm
When a word exceeds the page width, the DP algorithm would leave
dp[i] = MAX_COST, causing a cascade failure where all preceding
words also got MAX_COST. This resulted in each word being placed
on its own line.

Fix: When dp[i] remains MAX_COST after the inner loop, force the
oversized word onto its own line (ans[i] = i) and inherit the cost
from the next word (dp[i] = dp[i+1]) to allow preceding words to
find valid configurations.
2025-12-27 15:41:14 +09:00
Eunchurn Park
abe3e6c6db
fix(epub): prevent blank pages from corrupted index files
- Clean up incomplete temp files before retry attempts in Section.cpp
- Remove failed stream files immediately to prevent corruption
- Add data consistency validation in TextBlock deserialization (wc == xc == sc)
- Add sanity check for unreasonably large word counts (max 10000)
- Add iterator bounds validation before rendering to prevent overflow

This fixes an issue where pages after a certain point would appear blank
due to corrupted .bin files from failed SD card streaming retries.
2025-12-26 23:07:49 +09:00
5 changed files with 79 additions and 14 deletions

View File

@ -106,6 +106,18 @@ std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int
ans[i] = j; // j is the index of the last word in this optimal line ans[i] = j; // j is the index of the last word in this optimal line
} }
} }
// Handle oversized word: if no valid configuration found, force single-word line
// This prevents cascade failure where one oversized word breaks all preceding words
if (dp[i] == MAX_COST) {
ans[i] = i; // Just this word on its own line
// Inherit cost from next word to allow subsequent words to find valid configurations
if (i + 1 < static_cast<int>(totalWordCount)) {
dp[i] = dp[i + 1];
} else {
dp[i] = 0;
}
}
} }
// Stores the index of the word that starts the next line (last_word_index + 1) // Stores the index of the word that starts the next line (last_word_index + 1)

View File

@ -115,24 +115,39 @@ bool Section::clearCache() const {
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop, bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft, const int marginRight, const int marginBottom, const int marginLeft,
const bool extraParagraphSpacing, const std::function<void(int)>& progressFn) { const bool extraParagraphSpacing, const std::function<void()>& progressSetupFn,
const std::function<void(int)>& progressFn) {
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
// Retry logic for SD card timing issues // Retry logic for SD card timing issues
bool success = false; bool success = false;
size_t fileSize = 0;
for (int attempt = 0; attempt < 3 && !success; attempt++) { for (int attempt = 0; attempt < 3 && !success; attempt++) {
if (attempt > 0) { if (attempt > 0) {
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1); Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
delay(50); // Brief delay before retry delay(50); // Brief delay before retry
} }
// Remove any incomplete file from previous attempt before retrying
if (SD.exists(tmpHtmlPath.c_str())) {
SD.remove(tmpHtmlPath.c_str());
}
File tmpHtml; File tmpHtml;
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
continue; continue;
} }
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
fileSize = tmpHtml.size();
tmpHtml.close(); tmpHtml.close();
// If streaming failed, remove the incomplete file immediately
if (!success && SD.exists(tmpHtmlPath.c_str())) {
SD.remove(tmpHtmlPath.c_str());
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
}
} }
if (!success) { if (!success) {
@ -140,7 +155,12 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
return false; return false;
} }
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str()); Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
// Only show progress bar for larger chapters where rendering overhead is worth it
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
progressSetupFn();
}
ChapterHtmlSlimParser visitor( ChapterHtmlSlimParser visitor(
tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft,

View File

@ -33,6 +33,7 @@ class Section {
bool clearCache() const; bool clearCache() const;
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft, bool extraParagraphSpacing, int marginLeft, bool extraParagraphSpacing,
const std::function<void()>& progressSetupFn = nullptr,
const std::function<void(int)>& progressFn = nullptr); const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSD() const; std::unique_ptr<Page> loadPageFromSD() const;
}; };

View File

@ -4,11 +4,18 @@
#include <Serialization.h> #include <Serialization.h>
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
// Validate iterator bounds before rendering
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
return;
}
auto wordIt = words.begin(); auto wordIt = words.begin();
auto wordStylesIt = wordStyles.begin(); auto wordStylesIt = wordStyles.begin();
auto wordXposIt = wordXpos.begin(); auto wordXposIt = wordXpos.begin();
for (int i = 0; i < words.size(); i++) { for (size_t i = 0; i < words.size(); i++) {
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt); renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
std::advance(wordIt, 1); std::advance(wordIt, 1);
@ -46,6 +53,13 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
// words // words
serialization::readPod(file, wc); serialization::readPod(file, wc);
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
if (wc > 10000) {
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
return nullptr;
}
words.resize(wc); words.resize(wc);
for (auto& w : words) serialization::readString(file, w); for (auto& w : words) serialization::readString(file, w);
@ -59,6 +73,13 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
wordStyles.resize(sc); wordStyles.resize(sc);
for (auto& s : wordStyles) serialization::readPod(file, s); for (auto& s : wordStyles) serialization::readPod(file, s);
// Validate data consistency: all three lists must have the same size
if (wc != xc || wc != sc) {
Serial.printf("[%lu] [TXB] Deserialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), wc,
xc, sc);
return nullptr;
}
// style // style
serialization::readPod(file, style); serialization::readPod(file, style);

View File

@ -224,26 +224,37 @@ void EpubReaderActivity::renderScreen() {
constexpr int barHeight = 10; constexpr int barHeight = 10;
constexpr int boxMargin = 20; constexpr int boxMargin = 20;
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing..."); const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
const int boxWidth = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2; const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2;
const int boxHeight = renderer.getLineHeight(READER_FONT_ID) + barHeight + boxMargin * 3; const int boxWidthNoBar = textWidth + boxMargin * 2;
const int boxX = (GfxRenderer::getScreenWidth() - boxWidth) / 2; const int boxHeightWithBar = renderer.getLineHeight(READER_FONT_ID) + barHeight + boxMargin * 3;
const int boxHeightNoBar = renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2;
const int boxXWithBar = (GfxRenderer::getScreenWidth() - boxWidthWithBar) / 2;
const int boxXNoBar = (GfxRenderer::getScreenWidth() - boxWidthNoBar) / 2;
constexpr int boxY = 50; constexpr int boxY = 50;
const int barX = boxX + (boxWidth - barWidth) / 2; const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2;
const int barY = boxY + renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2; const int barY = boxY + renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2;
// Draw initial indexing box with 0% progress // Always show "Indexing..." text first
{ {
renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false);
renderer.drawText(READER_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Indexing..."); renderer.drawText(READER_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10);
// Draw empty progress bar outline
renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer(); renderer.displayBuffer();
pagesUntilFullRefresh = 0; pagesUntilFullRefresh = 0;
} }
section->setupCacheDir(); section->setupCacheDir();
// Setup callback - only called for chapters >= 50KB, redraws with progress bar
auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, boxMargin, barX, barY, barWidth,
barHeight]() {
renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false);
renderer.drawText(READER_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10);
renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer();
};
// Progress callback to update progress bar // Progress callback to update progress bar
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
const int fillWidth = (barWidth - 2) * progress / 100; const int fillWidth = (barWidth - 2) * progress / 100;
@ -252,7 +263,7 @@ void EpubReaderActivity::renderScreen() {
}; };
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft, SETTINGS.extraParagraphSpacing, progressCallback)) { marginLeft, SETTINGS.extraParagraphSpacing, progressSetup, progressCallback)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
section.reset(); section.reset();
return; return;