diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 907ad492..33379f26 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -49,6 +49,25 @@ std::unique_ptr PageImage::deserialize(FsFile& file) { return std::unique_ptr(new PageImage(std::move(ib), xPos, yPos)); } +void PageHR::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { + renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset); +} + +bool PageHR::serialize(FsFile& file) { + serialization::writePod(file, xPos); + serialization::writePod(file, yPos); + serialization::writePod(file, width); + return true; +} + +std::unique_ptr PageHR::deserialize(FsFile& file) { + int16_t xPos, yPos, width; + serialization::readPod(file, xPos); + serialization::readPod(file, yPos); + serialization::readPod(file, width); + return std::unique_ptr(new PageHR(xPos, yPos, width)); +} + void PageTableFragment::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { const int drawX = xPos + xOffset; const int drawY = yPos + yOffset; @@ -237,6 +256,10 @@ std::unique_ptr Page::deserialize(FsFile& file) { auto pt = PageTableFragment::deserialize(file); if (!pt) return nullptr; page->elements.push_back(std::move(pt)); + } else if (tag == TAG_PageHR) { + auto hr = PageHR::deserialize(file); + if (!hr) return nullptr; + page->elements.push_back(std::move(hr)); } else { LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag); return nullptr; diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 8b91fb92..828868ff 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -21,6 +21,7 @@ enum PageElementTag : uint8_t { TAG_PageLine = 1, TAG_PageImage = 2, TAG_PageTable = 3, + TAG_PageHR = 4, }; // represents something that has been added to a page @@ -63,6 +64,17 @@ class PageImage final : public PageElement { const ImageBlock& getImageBlock() const { return *imageBlock; } }; +class PageHR final : public PageElement { + int16_t width; + + public: + PageHR(const int16_t xPos, const int16_t yPos, const int16_t width) : PageElement(xPos, yPos), width(width) {} + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; + bool serialize(FsFile& file) override; + PageElementTag getTag() const override { return TAG_PageHR; } + static std::unique_ptr deserialize(FsFile& file); +}; + struct TableCell { std::vector> lines; bool isHeader = false; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index c44741d9..40d40993 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -14,30 +14,27 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 27; -constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION - sizeof(int) + // fontId - sizeof(float) + // lineCompression - sizeof(bool) + // extraParagraphSpacing - sizeof(uint8_t) + // paragraphAlignment - sizeof(uint16_t) + // viewportWidth - sizeof(uint16_t) + // viewportHeight - sizeof(bool) + // parseComplete - sizeof(uint16_t) + // pageCount (stored as 16-bit in header) - sizeof(bool) + // hyphenationEnabled - sizeof(bool) + // embeddedStyle - sizeof(bool) + // bionicReadingEnabled - sizeof(uint8_t) + // imageRendering - sizeof(uint32_t) + // page LUT offset - sizeof(uint32_t) + // anchor map offset - sizeof(uint32_t); // paragraph LUT offset +constexpr uint8_t SECTION_FILE_VERSION = 28; -constexpr uint32_t HEADER_TAIL_PARSE_COMPLETE_OFFSET = - HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(uint16_t) - sizeof(bool); -constexpr uint32_t HEADER_TAIL_PAGE_COUNT_OFFSET = HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(uint16_t); -constexpr uint32_t HEADER_TAIL_PAGE_LUT_OFFSET = HEADER_SIZE - sizeof(uint32_t) * 3; -constexpr uint32_t HEADER_TAIL_ANCHOR_OFFSET = HEADER_SIZE - sizeof(uint32_t) * 2; -constexpr uint32_t HEADER_TAIL_PARAGRAPH_LUT_OFFSET = HEADER_SIZE - sizeof(uint32_t); +namespace header { +constexpr uint32_t kVersion = 0; +constexpr uint32_t kFontId = kVersion + sizeof(uint8_t); +constexpr uint32_t kLineCompression = kFontId + sizeof(int); +constexpr uint32_t kExtraParagraphSpacing = kLineCompression + sizeof(float); +constexpr uint32_t kParagraphAlignment = kExtraParagraphSpacing + sizeof(bool); +constexpr uint32_t kViewportWidth = kParagraphAlignment + sizeof(uint8_t); +constexpr uint32_t kViewportHeight = kViewportWidth + sizeof(uint16_t); +constexpr uint32_t kHyphenationEnabled = kViewportHeight + sizeof(uint16_t); +constexpr uint32_t kEmbeddedStyle = kHyphenationEnabled + sizeof(bool); +constexpr uint32_t kBionicReadingEnabled = kEmbeddedStyle + sizeof(bool); +constexpr uint32_t kImageRendering = kBionicReadingEnabled + sizeof(bool); +constexpr uint32_t kParseComplete = kImageRendering + sizeof(uint8_t); +constexpr uint32_t kPageCount = kParseComplete + sizeof(bool); +constexpr uint32_t kPageLut = kPageCount + sizeof(uint16_t); +constexpr uint32_t kAnchorMap = kPageLut + sizeof(uint32_t); +constexpr uint32_t kParagraphLut = kAnchorMap + sizeof(uint32_t); +constexpr uint32_t kSize = kParagraphLut + sizeof(uint32_t); +} // namespace header // On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex + u16 listItemIndex. // listItemIndex is the running
  • count at page-break time; together with @@ -217,11 +214,12 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi LOG_DBG("SCT", "File not open for writing header"); return; } - static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + - sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + - sizeof(viewportHeight) + sizeof(bool) + sizeof(pageCount) + - sizeof(hyphenationEnabled) + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + - sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t), + static_assert(header::kSize == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + + sizeof(viewportWidth) + sizeof(viewportHeight) + sizeof(hyphenationEnabled) + + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) + + sizeof(bool) + sizeof(pageCount) + sizeof(uint32_t) + sizeof(uint32_t) + + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); @@ -338,8 +336,8 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con } for (uint32_t& pos : lut) { serialization::readPod(file, pos); - if (pos < HEADER_SIZE || pos >= lutOffset) { - LOG_ERR("SCT", "Deserialization failed: LUT entry %u out of range [%u, %u)", pos, HEADER_SIZE, lutOffset); + if (pos < header::kSize || pos >= lutOffset) { + LOG_ERR("SCT", "Deserialization failed: LUT entry %u out of range [%u, %u)", pos, header::kSize, lutOffset); clearCache(); return false; } @@ -572,9 +570,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } // Patch header with final parseComplete/pageCount and offsets. - const size_t headerPatchStart = HEADER_TAIL_PARSE_COMPLETE_OFFSET; + const size_t headerPatchStart = header::kParseComplete; if (!file.seek(headerPatchStart)) { - LOG_ERR("SCT", "Failed to seek to section header patch offset %u", HEADER_TAIL_PARSE_COMPLETE_OFFSET); + LOG_ERR("SCT", "Failed to seek to section header patch offset %u", header::kParseComplete); file.close(); Storage.remove(filePath.c_str()); return false; @@ -730,8 +728,7 @@ void Section::buildTocBoundariesFromFile(FsFile& f) { // Single pass through on-disk anchors, matching against cached TOC anchors. // Stop early once all TOC anchors are resolved. - // Header layout: ... | lutOffset (u32) | anchorMapOffset (u32) | paragraphLutOffset (u32) | - f.seek(HEADER_TAIL_ANCHOR_OFFSET); + f.seek(header::kAnchorMap); uint32_t anchorMapOffset; serialization::readPod(f, anchorMapOffset); @@ -801,7 +798,7 @@ std::optional Section::getPageForAnchor(const std::string& anchor) con } const uint32_t fileSize = f.size(); - f.seek(HEADER_TAIL_ANCHOR_OFFSET); + f.seek(header::kAnchorMap); uint32_t anchorMapOffset; serialization::readPod(f, anchorMapOffset); if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) { @@ -834,7 +831,7 @@ bool Section::readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32 const uint32_t fileSize = outFile.size(); - outFile.seek(HEADER_TAIL_PARAGRAPH_LUT_OFFSET); + outFile.seek(header::kParagraphLut); uint32_t paragraphLutOffset; serialization::readPod(outFile, paragraphLutOffset); if (fileSize < sizeof(uint16_t) || paragraphLutOffset == 0 || paragraphLutOffset > fileSize - sizeof(uint16_t)) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 2918e889..79ae15fb 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -1064,6 +1064,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->preUntilDepth = std::min(self->preUntilDepth, self->depth); } } + } else if (strcmp(name, "hr") == 0) { + if (self->partWordBufferIndex > 0) { + if (!self->flushPartWordBuffer()) return; + } + self->makePages(); + if (!self->currentPage) { + self->currentPage.reset(new Page()); + self->currentPageNextY = 0; + } + const int lineHeight = static_cast(self->renderer.getLineHeight(self->fontId) * self->lineCompression + 0.5f); + const int16_t marginV = static_cast(lineHeight / 2); + self->currentPageNextY += marginV; + if (self->currentPageNextY + 1 + marginV > self->viewportHeight) { + self->emitPage(self->lastBodyChildByteOffset); + self->currentPage.reset(new Page()); + self->currentPageNextY = 0; + } + self->currentPage->elements.push_back( + std::make_shared(0, self->currentPageNextY, static_cast(self->viewportWidth))); + self->currentPageNextY += 1 + marginV; + BlockStyle emptyStyle; + self->startNewTextBlock(emptyStyle); } else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS)) { // Flush buffer before style change so preceding text gets current style diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index b660a3ce..19e6329f 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -30,6 +30,8 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title int8_t bionicReadingOverride = -1; int8_t paragraphAlignmentOverride = -1; + pruneMissing(); + // Remove existing entry if present auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); @@ -66,6 +68,14 @@ void RecentBooksStore::removeBook(const std::string& path) { } } +bool RecentBooksStore::isMissing(const RecentBook& book) { return !Storage.exists(book.path.c_str()); } + +bool RecentBooksStore::pruneMissing() { + const size_t before = recentBooks.size(); + recentBooks.erase(std::remove_if(recentBooks.begin(), recentBooks.end(), &isMissing), recentBooks.end()); + return recentBooks.size() != before; +} + void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, const std::string& series, const std::string& coverBmpPath) { auto it = diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 2df63c25..aedb21ca 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -62,6 +62,13 @@ class RecentBooksStore { // Get the count of recent books int getCount() const { return static_cast(recentBooks.size()); } + // Returns true if the book's file is missing from storage + static bool isMissing(const RecentBook& book); + + // Remove entries whose backing file is no longer on the SD card. + // Returns true if any entry was removed. Does not persist — caller decides. + bool pruneMissing(); + bool saveToFile() const; bool loadFromFile(); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 868650a4..a30358bc 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -168,8 +168,7 @@ void HomeActivity::loadRecentBooks(int maxBooks) { break; } - // Skip if file no longer exists - if (!Storage.exists(book.path.c_str())) { + if (RecentBooksStore::isMissing(book)) { continue; } diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index b3746614..3934416e 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -68,15 +68,7 @@ std::string gridThumbPath(const std::string& coverBmpPath, int tw, int th) { } } // namespace -void RecentBooksActivity::loadRecentBooks() { - recentBooks.clear(); - const auto& books = RECENT_BOOKS.getBooks(); - recentBooks.reserve(books.size()); - for (const auto& book : books) { - if (!Storage.exists(book.path.c_str())) continue; - recentBooks.push_back(book); - } -} +void RecentBooksActivity::loadRecentBooks() { recentBooks = RECENT_BOOKS.getBooks(); } bool RecentBooksActivity::loadNextCover() { const Rect contentRect = UITheme::getContentRect(renderer, true, true); @@ -134,6 +126,10 @@ bool RecentBooksActivity::loadNextCover() { void RecentBooksActivity::onEnter() { Activity::onEnter(); + if (RECENT_BOOKS.pruneMissing()) { + RECENT_BOOKS.saveToFile(); + } + loadRecentBooks(); selectorIndex = 0; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 9d63b9eb..9aa114b0 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -15,6 +15,7 @@ #include "CrossPointSettings.h" #include "FontInstaller.h" +#include "HttpFileStreamer.h" #include "OpdsServerStore.h" #include "SdCardFontGlobals.h" #include "SdCardFontRegistry.h" @@ -681,27 +682,12 @@ void CrossPointWebServer::handleDownload() const { server->send(200, contentType.c_str(), ""); NetworkClient client = server->client(); - const size_t chunkSize = 4096; - uint8_t buffer[chunkSize]; - - bool downloadOk = true; - while (downloadOk && file.available()) { - int result = file.read(buffer, chunkSize); - if (result <= 0) break; - size_t bytesRead = static_cast(result); - size_t totalWritten = 0; - while (totalWritten < bytesRead) { - esp_task_wdt_reset(); - size_t wrote = client.write(buffer + totalWritten, bytesRead - totalWritten); - if (wrote == 0) { - downloadOk = false; - break; - } - totalWritten += wrote; - } - } + bool downloadOk = HttpFileStreamer::streamFileToClient(file, client); client.clear(); - file.close(); + + if (!downloadOk) { + LOG_DBG("WEB", "Download interrupted while streaming: %s", itemPath.c_str()); + } } // Diagnostic counters for upload performance analysis diff --git a/src/network/HttpFileStreamer.cpp b/src/network/HttpFileStreamer.cpp new file mode 100644 index 00000000..571b0cc9 --- /dev/null +++ b/src/network/HttpFileStreamer.cpp @@ -0,0 +1,44 @@ +#include "HttpFileStreamer.h" + +#include +#include + +namespace { +constexpr size_t DOWNLOAD_CHUNK_SIZE = 4096; +} + +namespace HttpFileStreamer { +bool streamFileToClient(FsFile& file, NetworkClient& client) { + auto* buffer = static_cast(malloc(DOWNLOAD_CHUNK_SIZE)); + if (!buffer) { + LOG_ERR("HTTP", "malloc failed: %zu bytes", DOWNLOAD_CHUNK_SIZE); + return false; + } + + bool ok = true; + while (ok) { + esp_task_wdt_reset(); + int result = file.read(buffer, DOWNLOAD_CHUNK_SIZE); + if (result < 0) { + ok = false; + break; + } + if (result == 0) break; + + size_t bytesRead = static_cast(result); + size_t totalWritten = 0; + while (totalWritten < bytesRead) { + esp_task_wdt_reset(); + size_t wrote = client.write(buffer + totalWritten, bytesRead - totalWritten); + if (wrote == 0) { + ok = false; + break; + } + totalWritten += wrote; + } + } + + free(buffer); + return ok; +} +} // namespace HttpFileStreamer diff --git a/src/network/HttpFileStreamer.h b/src/network/HttpFileStreamer.h new file mode 100644 index 00000000..91bb6276 --- /dev/null +++ b/src/network/HttpFileStreamer.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace HttpFileStreamer { +bool streamFileToClient(FsFile& file, NetworkClient& client); +} diff --git a/src/network/WebDAVHandler.cpp b/src/network/WebDAVHandler.cpp index 46ae4450..9dff1bf4 100644 --- a/src/network/WebDAVHandler.cpp +++ b/src/network/WebDAVHandler.cpp @@ -8,6 +8,8 @@ #include #include +#include "HttpFileStreamer.h" + namespace { const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"}; constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); @@ -330,8 +332,12 @@ void WebDAVHandler::handleGet(WebServer& s) { s.send(200, contentType.c_str(), ""); NetworkClient client = s.client(); - client.write(file); - file.close(); + bool downloadOk = HttpFileStreamer::streamFileToClient(file, client); + client.clear(); + + if (!downloadOk) { + LOG_DBG("DAV", "GET interrupted while streaming: %s", path.c_str()); + } } // ── HEAD ─────────────────────────────────────────────────────────────────────