2025-12-03 22:00:29 +11:00
|
|
|
#include "Section.h"
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
#include <SDCardManager.h>
|
2025-12-12 22:13:34 +11:00
|
|
|
#include <Serialization.h>
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
#include "Page.h"
|
2026-01-19 17:56:26 +05:00
|
|
|
#include "hyphenation/Hyphenator.h"
|
2025-12-13 19:36:01 +11:00
|
|
|
#include "parsers/ChapterHtmlSlimParser.h"
|
2025-12-06 22:04:59 +11:00
|
|
|
|
2025-12-17 23:32:18 +11:00
|
|
|
namespace {
|
2026-01-20 10:27:55 -06:00
|
|
|
constexpr uint8_t SECTION_FILE_VERSION = 11;
|
2026-01-02 01:21:48 -06:00
|
|
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
2026-01-19 17:56:26 +05:00
|
|
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
|
|
|
|
sizeof(uint32_t);
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
} // namespace
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
2025-12-29 12:19:54 +10:00
|
|
|
if (!file) {
|
|
|
|
|
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
|
|
|
|
return 0;
|
2025-12-23 14:14:10 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
const uint32_t position = file.position();
|
2025-12-29 12:19:54 +10:00
|
|
|
if (!page->serialize(file)) {
|
|
|
|
|
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
|
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
pageCount++;
|
2025-12-29 12:19:54 +10:00
|
|
|
return position;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 12:19:54 +10:00
|
|
|
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
2026-01-02 01:21:48 -06:00
|
|
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
2026-01-19 17:56:26 +05:00
|
|
|
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
2025-12-29 12:19:54 +10:00
|
|
|
if (!file) {
|
|
|
|
|
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
2025-12-23 14:14:10 +11:00
|
|
|
return;
|
|
|
|
|
}
|
2025-12-29 12:19:54 +10:00
|
|
|
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
2026-01-02 01:21:48 -06:00
|
|
|
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
2026-01-19 17:56:26 +05:00
|
|
|
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
|
|
|
|
sizeof(uint32_t),
|
2025-12-29 12:19:54 +10:00
|
|
|
"Header size mismatch");
|
|
|
|
|
serialization::writePod(file, SECTION_FILE_VERSION);
|
|
|
|
|
serialization::writePod(file, fontId);
|
|
|
|
|
serialization::writePod(file, lineCompression);
|
|
|
|
|
serialization::writePod(file, extraParagraphSpacing);
|
2026-01-02 01:21:48 -06:00
|
|
|
serialization::writePod(file, paragraphAlignment);
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::writePod(file, viewportWidth);
|
|
|
|
|
serialization::writePod(file, viewportHeight);
|
2026-01-19 17:56:26 +05:00
|
|
|
serialization::writePod(file, hyphenationEnabled);
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
2025-12-30 15:09:30 +10:00
|
|
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
|
2025-12-06 22:04:59 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 12:19:54 +10:00
|
|
|
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
2026-01-02 01:21:48 -06:00
|
|
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
2026-01-19 17:56:26 +05:00
|
|
|
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
2025-12-03 22:00:29 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
// Match parameters
|
|
|
|
|
{
|
|
|
|
|
uint8_t version;
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::readPod(file, version);
|
2025-12-08 22:06:09 +11:00
|
|
|
if (version != SECTION_FILE_VERSION) {
|
2025-12-29 12:19:54 +10:00
|
|
|
file.close();
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
2025-12-12 22:13:34 +11:00
|
|
|
clearCache();
|
2025-12-08 22:06:09 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 12:11:36 +10:00
|
|
|
int fileFontId;
|
|
|
|
|
uint16_t fileViewportWidth, fileViewportHeight;
|
2025-12-08 22:06:09 +11:00
|
|
|
float fileLineCompression;
|
2025-12-15 13:16:46 +01:00
|
|
|
bool fileExtraParagraphSpacing;
|
2026-01-02 01:21:48 -06:00
|
|
|
uint8_t fileParagraphAlignment;
|
2026-01-19 17:56:26 +05:00
|
|
|
bool fileHyphenationEnabled;
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::readPod(file, fileFontId);
|
|
|
|
|
serialization::readPod(file, fileLineCompression);
|
|
|
|
|
serialization::readPod(file, fileExtraParagraphSpacing);
|
2026-01-02 01:21:48 -06:00
|
|
|
serialization::readPod(file, fileParagraphAlignment);
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::readPod(file, fileViewportWidth);
|
|
|
|
|
serialization::readPod(file, fileViewportHeight);
|
2026-01-19 17:56:26 +05:00
|
|
|
serialization::readPod(file, fileHyphenationEnabled);
|
2025-12-08 22:06:09 +11:00
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
2026-01-02 01:21:48 -06:00
|
|
|
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
2026-01-19 17:56:26 +05:00
|
|
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
|
|
|
|
hyphenationEnabled != fileHyphenationEnabled) {
|
2025-12-29 12:19:54 +10:00
|
|
|
file.close();
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
2025-12-12 22:13:34 +11:00
|
|
|
clearCache();
|
2025-12-08 22:06:09 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-06 22:04:59 +11:00
|
|
|
}
|
2025-12-08 22:06:09 +11:00
|
|
|
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::readPod(file, pageCount);
|
|
|
|
|
file.close();
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
2025-12-03 22:00:29 +11:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
|
|
|
|
bool Section::clearCache() const {
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!SdMan.exists(filePath.c_str())) {
|
2025-12-12 22:13:34 +11:00
|
|
|
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!SdMan.remove(filePath.c_str())) {
|
2025-12-12 22:13:34 +11:00
|
|
|
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 12:19:54 +10:00
|
|
|
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
2026-01-02 01:21:48 -06:00
|
|
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
2026-01-19 17:56:26 +05:00
|
|
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
|
|
|
|
const std::function<void()>& progressSetupFn,
|
2025-12-29 12:19:54 +10:00
|
|
|
const std::function<void(int)>& progressFn) {
|
2025-12-30 15:09:30 +10:00
|
|
|
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
2025-12-24 22:36:13 +11:00
|
|
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
2025-12-03 22:00:29 +11:00
|
|
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
2025-12-28 13:59:44 +09:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
// Create cache directory if it doesn't exist
|
|
|
|
|
{
|
|
|
|
|
const auto sectionsDir = epub->getCachePath() + "/sections";
|
|
|
|
|
SdMan.mkdir(sectionsDir.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 13:59:44 +09:00
|
|
|
// Retry logic for SD card timing issues
|
|
|
|
|
bool success = false;
|
2025-12-30 15:09:30 +10:00
|
|
|
uint32_t fileSize = 0;
|
2025-12-28 13:59:44 +09:00
|
|
|
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
|
|
|
|
if (attempt > 0) {
|
|
|
|
|
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
|
|
|
|
delay(50); // Brief delay before retry
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove any incomplete file from previous attempt before retrying
|
2025-12-30 15:09:30 +10:00
|
|
|
if (SdMan.exists(tmpHtmlPath.c_str())) {
|
|
|
|
|
SdMan.remove(tmpHtmlPath.c_str());
|
2025-12-28 13:59:44 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
FsFile tmpHtml;
|
|
|
|
|
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
2025-12-28 13:59:44 +09:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
|
|
|
|
fileSize = tmpHtml.size();
|
|
|
|
|
tmpHtml.close();
|
|
|
|
|
|
|
|
|
|
// If streaming failed, remove the incomplete file immediately
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!success && SdMan.exists(tmpHtmlPath.c_str())) {
|
|
|
|
|
SdMan.remove(tmpHtmlPath.c_str());
|
2025-12-28 13:59:44 +09:00
|
|
|
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
|
|
|
|
}
|
2025-12-23 14:14:10 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
if (!success) {
|
2025-12-28 13:59:44 +09:00
|
|
|
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
2025-12-03 22:00:29 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 13:59:44 +09:00
|
|
|
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();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
2025-12-29 12:19:54 +10:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-02 01:21:48 -06:00
|
|
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
2026-01-19 17:56:26 +05:00
|
|
|
viewportHeight, hyphenationEnabled);
|
2025-12-30 15:09:30 +10:00
|
|
|
std::vector<uint32_t> lut = {};
|
2025-12-29 12:19:54 +10:00
|
|
|
|
2025-12-28 13:59:44 +09:00
|
|
|
ChapterHtmlSlimParser visitor(
|
2026-01-02 01:21:48 -06:00
|
|
|
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
2026-01-19 17:56:26 +05:00
|
|
|
viewportHeight, hyphenationEnabled,
|
2026-01-19 22:39:40 -06:00
|
|
|
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, progressFn,
|
|
|
|
|
epub->getCssParser());
|
2026-01-19 17:56:26 +05:00
|
|
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
2025-12-08 00:39:17 +11:00
|
|
|
success = visitor.parseAndBuildPages();
|
2025-12-06 20:57:24 +11:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
SdMan.remove(tmpHtmlPath.c_str());
|
2025-12-03 22:00:29 +11:00
|
|
|
if (!success) {
|
2026-01-24 21:10:08 -05:00
|
|
|
Serial.printf("[%lu] [SCT] Failed to parse XML, creating placeholder page for chapter\n", millis());
|
|
|
|
|
// Create a placeholder page for malformed chapters instead of failing entirely
|
|
|
|
|
// This allows the book to continue loading with chapters that do parse successfully
|
|
|
|
|
auto placeholderPage = std::unique_ptr<Page>(new Page());
|
|
|
|
|
// Add placeholder to LUT
|
|
|
|
|
lut.emplace_back(this->onPageComplete(std::move(placeholderPage)));
|
|
|
|
|
|
|
|
|
|
// If we still have no pages, the placeholder creation failed
|
|
|
|
|
if (pageCount == 0) {
|
|
|
|
|
Serial.printf("[%lu] [SCT] Failed to create placeholder page\n", millis());
|
|
|
|
|
file.close();
|
|
|
|
|
SdMan.remove(filePath.c_str());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
const uint32_t lutOffset = file.position();
|
2025-12-29 12:19:54 +10:00
|
|
|
bool hasFailedLutRecords = false;
|
|
|
|
|
// Write LUT
|
2025-12-30 15:09:30 +10:00
|
|
|
for (const uint32_t& pos : lut) {
|
2025-12-29 12:19:54 +10:00
|
|
|
if (pos == 0) {
|
|
|
|
|
hasFailedLutRecords = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
serialization::writePod(file, pos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasFailedLutRecords) {
|
|
|
|
|
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
|
|
|
|
|
file.close();
|
2025-12-30 15:09:30 +10:00
|
|
|
SdMan.remove(filePath.c_str());
|
2025-12-29 12:19:54 +10:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 12:19:54 +10:00
|
|
|
// Go back and write LUT offset
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount));
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::writePod(file, pageCount);
|
|
|
|
|
serialization::writePod(file, lutOffset);
|
|
|
|
|
file.close();
|
2025-12-03 22:00:29 +11:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 12:19:54 +10:00
|
|
|
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
2025-12-08 19:48:49 +11:00
|
|
|
return nullptr;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-29 12:19:54 +10:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
|
|
|
|
uint32_t lutOffset;
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::readPod(file, lutOffset);
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
|
|
|
|
uint32_t pagePos;
|
2025-12-29 12:19:54 +10:00
|
|
|
serialization::readPod(file, pagePos);
|
|
|
|
|
file.seek(pagePos);
|
|
|
|
|
|
|
|
|
|
auto page = Page::deserialize(file);
|
|
|
|
|
file.close();
|
2025-12-12 22:13:34 +11:00
|
|
|
return page;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|