diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..66bb8c9 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-std=c++2a] diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 938fe55..dcfbe1e 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -6,6 +6,8 @@ #include +#include "Epub/FsHelpers.h" + bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) { // open up the meta data to find where the content.opf file lives size_t s; @@ -249,7 +251,20 @@ bool Epub::load() { return true; } -void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); } +bool Epub::clearCache() const { + if (!SD.exists(cachePath.c_str())) { + Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis()); + return true; + } + + if (!FsHelpers::removeDir(cachePath.c_str())) { + Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis()); + return false; + } + + Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis()); + return true; +} void Epub::setupCacheDir() const { if (SD.exists(cachePath.c_str())) { diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index b1cf4a9..5cdfee4 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -50,7 +50,7 @@ class Epub { ~Epub() = default; std::string& getBasePath() { return contentBasePath; } bool load(); - void clearCache() const; + bool clearCache() const; void setupCacheDir() const; const std::string& getCachePath() const; const std::string& getPath() const; diff --git a/lib/Epub/Epub/EpubHtmlParserSlim.cpp b/lib/Epub/Epub/EpubHtmlParserSlim.cpp index 3b0db63..9d7ef52 100644 --- a/lib/Epub/Epub/EpubHtmlParserSlim.cpp +++ b/lib/Epub/Epub/EpubHtmlParserSlim.cpp @@ -38,7 +38,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib } // start a new text block if needed -void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) { +void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) { if (currentTextBlock) { // already have a text block running and it is empty - just reuse it if (currentTextBlock->isEmpty()) { @@ -46,11 +46,9 @@ void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) { return; } - currentTextBlock->finish(); makePages(); - delete currentTextBlock; } - currentTextBlock = new TextBlock(style); + currentTextBlock.reset(new ParsedText(style)); } void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { @@ -94,13 +92,13 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na } if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { - self->startNewTextBlock(CENTER_ALIGN); + self->startNewTextBlock(TextBlock::CENTER_ALIGN); self->boldUntilDepth = min(self->boldUntilDepth, self->depth); } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { if (strcmp(name, "br") == 0) { self->startNewTextBlock(self->currentTextBlock->getStyle()); } else { - self->startNewTextBlock(JUSTIFIED); + self->startNewTextBlock(TextBlock::JUSTIFIED); } } else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { self->boldUntilDepth = min(self->boldUntilDepth, self->depth); @@ -119,13 +117,21 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s return; } + EpdFontStyle fontStyle = REGULAR; + if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) { + fontStyle = BOLD_ITALIC; + } else if (self->boldUntilDepth < self->depth) { + fontStyle = BOLD; + } else if (self->italicUntilDepth < self->depth) { + fontStyle = ITALIC; + } + for (int i = 0; i < len; i++) { if (isWhitespace(s[i])) { // Currently looking at whitespace, if there's anything in the partWordBuffer, flush it if (self->partWordBufferIndex > 0) { self->partWordBuffer[self->partWordBufferIndex] = '\0'; - self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, - self->italicUntilDepth < self->depth); + self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle); self->partWordBufferIndex = 0; } // Skip the whitespace char @@ -135,8 +141,7 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s // If we're about to run out of space, then cut the word off and start a new one if (self->partWordBufferIndex >= MAX_WORD_SIZE) { self->partWordBuffer[self->partWordBufferIndex] = '\0'; - self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, - self->italicUntilDepth < self->depth); + self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle); self->partWordBufferIndex = 0; } @@ -158,9 +163,17 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1; if (shouldBreakText) { + EpdFontStyle fontStyle = REGULAR; + if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) { + fontStyle = BOLD_ITALIC; + } else if (self->boldUntilDepth < self->depth) { + fontStyle = BOLD; + } else if (self->italicUntilDepth < self->depth) { + fontStyle = ITALIC; + } + self->partWordBuffer[self->partWordBufferIndex] = '\0'; - self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, - self->italicUntilDepth < self->depth); + self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle); self->partWordBufferIndex = 0; } } @@ -184,7 +197,7 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name } bool EpubHtmlParserSlim::parseAndBuildPages() { - startNewTextBlock(JUSTIFIED); + startNewTextBlock(TextBlock::JUSTIFIED); const XML_Parser parser = XML_ParserCreate(nullptr); int done; @@ -240,10 +253,9 @@ bool EpubHtmlParserSlim::parseAndBuildPages() { // Process last page if there is still text if (currentTextBlock) { makePages(); - completePageFn(currentPage); - currentPage = nullptr; - delete currentTextBlock; - currentTextBlock = nullptr; + completePageFn(std::move(currentPage)); + currentPage.reset(); + currentTextBlock.reset(); } return true; @@ -256,7 +268,7 @@ void EpubHtmlParserSlim::makePages() { } if (!currentPage) { - currentPage = new Page(); + currentPage.reset(new Page()); currentPageNextY = marginTop; } @@ -266,30 +278,18 @@ void EpubHtmlParserSlim::makePages() { // Long running task, make sure to let other things happen vTaskDelay(1); - if (currentTextBlock->getType() == TEXT_BLOCK) { - const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight); + const auto lines = currentTextBlock->layoutAndExtractLines(renderer, fontId, marginLeft + marginRight); - for (const auto line : lines) { - if (currentPageNextY + lineHeight > pageHeight) { - completePageFn(currentPage); - currentPage = new Page(); - currentPageNextY = marginTop; - } - - currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY)); - currentPageNextY += lineHeight; + for (auto&& line : lines) { + if (currentPageNextY + lineHeight > pageHeight) { + completePageFn(std::move(currentPage)); + currentPage.reset(new Page()); + currentPageNextY = marginTop; } - // add some extra line between blocks - currentPageNextY += lineHeight / 2; + + currentPage->elements.push_back(std::make_shared(line, marginLeft, currentPageNextY)); + currentPageNextY += lineHeight; } - // TODO: Image block support - // if (block->getType() == BlockType::IMAGE_BLOCK) { - // ImageBlock *imageBlock = (ImageBlock *)block; - // if (y + imageBlock->height > page_height) { - // pages.push_back(new Page()); - // y = 0; - // } - // pages.back()->elements.push_back(new PageImage(imageBlock, y)); - // y += imageBlock->height; - // } + // add some extra line between blocks + currentPageNextY += lineHeight / 2; } diff --git a/lib/Epub/Epub/EpubHtmlParserSlim.h b/lib/Epub/Epub/EpubHtmlParserSlim.h index dafff79..971e93f 100644 --- a/lib/Epub/Epub/EpubHtmlParserSlim.h +++ b/lib/Epub/Epub/EpubHtmlParserSlim.h @@ -4,7 +4,9 @@ #include #include +#include +#include "ParsedText.h" #include "blocks/TextBlock.h" class Page; @@ -15,7 +17,7 @@ class GfxRenderer; class EpubHtmlParserSlim { const char* filepath; GfxRenderer& renderer; - std::function completePageFn; + std::function)> completePageFn; int depth = 0; int skipUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX; @@ -24,8 +26,8 @@ class EpubHtmlParserSlim { // leave one char at end for null pointer char partWordBuffer[MAX_WORD_SIZE + 1] = {}; int partWordBufferIndex = 0; - TextBlock* currentTextBlock = nullptr; - Page* currentPage = nullptr; + std::unique_ptr currentTextBlock = nullptr; + std::unique_ptr currentPage = nullptr; int currentPageNextY = 0; int fontId; float lineCompression; @@ -34,7 +36,7 @@ class EpubHtmlParserSlim { int marginBottom; int marginLeft; - void startNewTextBlock(BLOCK_STYLE style); + void startNewTextBlock(TextBlock::BLOCK_STYLE style); void makePages(); // XML callbacks static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); @@ -45,7 +47,7 @@ class EpubHtmlParserSlim { explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft, - const std::function& completePageFn) + const std::function)>& completePageFn) : filepath(filepath), renderer(renderer), fontId(fontId), diff --git a/lib/Epub/Epub/FsHelpers.cpp b/lib/Epub/Epub/FsHelpers.cpp new file mode 100644 index 0000000..5287252 --- /dev/null +++ b/lib/Epub/Epub/FsHelpers.cpp @@ -0,0 +1,36 @@ +#include "FsHelpers.h" + +#include + +bool FsHelpers::removeDir(const char* path) { + // 1. Open the directory + File dir = SD.open(path); + if (!dir) { + return false; + } + if (!dir.isDirectory()) { + return false; + } + + File file = dir.openNextFile(); + while (file) { + String filePath = path; + if (!filePath.endsWith("/")) { + filePath += "/"; + } + filePath += file.name(); + + if (file.isDirectory()) { + if (!removeDir(filePath.c_str())) { + return false; + } + } else { + if (!SD.remove(filePath.c_str())) { + return false; + } + } + file = dir.openNextFile(); + } + + return SD.rmdir(path); +} diff --git a/lib/Epub/Epub/FsHelpers.h b/lib/Epub/Epub/FsHelpers.h new file mode 100644 index 0000000..bc5204b --- /dev/null +++ b/lib/Epub/Epub/FsHelpers.h @@ -0,0 +1,6 @@ +#pragma once + +class FsHelpers { + public: + static bool removeDir(const char* path); +}; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 5f80047..2eda0a1 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -3,7 +3,7 @@ #include #include -constexpr uint8_t PAGE_FILE_VERSION = 1; +constexpr uint8_t PAGE_FILE_VERSION = 2; void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } @@ -15,14 +15,14 @@ void PageLine::serialize(std::ostream& os) { block->serialize(os); } -PageLine* PageLine::deserialize(std::istream& is) { +std::unique_ptr PageLine::deserialize(std::istream& is) { int32_t xPos; int32_t yPos; serialization::readPod(is, xPos); serialization::readPod(is, yPos); - const auto tb = TextBlock::deserialize(is); - return new PageLine(tb, xPos, yPos); + auto tb = TextBlock::deserialize(is); + return std::unique_ptr(new PageLine(std::move(tb), xPos, yPos)); } void Page::render(GfxRenderer& renderer, const int fontId) const { @@ -37,14 +37,14 @@ void Page::serialize(std::ostream& os) const { const uint32_t count = elements.size(); serialization::writePod(os, count); - for (auto* el : elements) { + for (const auto& el : elements) { // Only PageLine exists currently serialization::writePod(os, static_cast(TAG_PageLine)); - static_cast(el)->serialize(os); + el->serialize(os); } } -Page* Page::deserialize(std::istream& is) { +std::unique_ptr Page::deserialize(std::istream& is) { uint8_t version; serialization::readPod(is, version); if (version != PAGE_FILE_VERSION) { @@ -52,7 +52,7 @@ Page* Page::deserialize(std::istream& is) { return nullptr; } - auto* page = new Page(); + auto page = std::unique_ptr(new Page()); uint32_t count; serialization::readPod(is, count); @@ -62,10 +62,11 @@ Page* Page::deserialize(std::istream& is) { serialization::readPod(is, tag); if (tag == TAG_PageLine) { - auto* pl = PageLine::deserialize(is); - page->elements.push_back(pl); + auto pl = PageLine::deserialize(is); + page->elements.push_back(std::move(pl)); } else { - throw std::runtime_error("Unknown PageElement tag"); + Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag); + return nullptr; } } diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 9d014af..f7ff1c7 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -1,4 +1,7 @@ #pragma once +#include +#include + #include "blocks/TextBlock.h" enum PageElementTag : uint8_t { @@ -18,27 +21,21 @@ class PageElement { // a line from a block element class PageLine final : public PageElement { - const TextBlock* block; + std::shared_ptr block; public: - PageLine(const TextBlock* block, const int xPos, const int yPos) : PageElement(xPos, yPos), block(block) {} - ~PageLine() override { delete block; } + PageLine(std::shared_ptr block, const int xPos, const int yPos) + : PageElement(xPos, yPos), block(std::move(block)) {} void render(GfxRenderer& renderer, int fontId) override; void serialize(std::ostream& os) override; - static PageLine* deserialize(std::istream& is); + static std::unique_ptr deserialize(std::istream& is); }; class Page { public: - ~Page() { - for (const auto element : elements) { - delete element; - } - } - // the list of block index and line numbers on this page - std::vector elements; + std::vector> elements; void render(GfxRenderer& renderer, int fontId) const; void serialize(std::ostream& os) const; - static Page* deserialize(std::istream& is); + static std::unique_ptr deserialize(std::istream& is); }; diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp new file mode 100644 index 0000000..8e050a5 --- /dev/null +++ b/lib/Epub/Epub/ParsedText.cpp @@ -0,0 +1,167 @@ +#include "ParsedText.h" + +#include + +#include +#include +#include +#include + +constexpr int MAX_COST = std::numeric_limits::max(); + +void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) { + if (word.empty()) return; + + words.push_back(std::move(word)); + wordStyles.push_back(fontStyle); +} + +// Consumes data to minimize memory usage +std::list> ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, + const int horizontalMargin) { + if (words.empty()) { + return {}; + } + + const size_t totalWordCount = words.size(); + const int pageWidth = renderer.getScreenWidth() - horizontalMargin; + const int spaceWidth = renderer.getSpaceWidth(fontId); + + std::vector wordWidths; + wordWidths.reserve(totalWordCount); + + auto wordsIt = words.begin(); + auto wordStylesIt = wordStyles.begin(); + + while (wordsIt != words.end()) { + wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt)); + + std::advance(wordsIt, 1); + std::advance(wordStylesIt, 1); + } + + // DP table to store the minimum badness (cost) of lines starting at index i + std::vector dp(totalWordCount); + // 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i' + std::vector ans(totalWordCount); + + // Base Case + dp[totalWordCount - 1] = 0; + ans[totalWordCount - 1] = totalWordCount - 1; + + for (int i = totalWordCount - 2; i >= 0; --i) { + int currlen = -spaceWidth; + dp[i] = MAX_COST; + + for (size_t j = i; j < totalWordCount; ++j) { + // Current line length: previous width + space + current word width + currlen += wordWidths[j] + spaceWidth; + + if (currlen > pageWidth) { + break; + } + + int cost; + if (j == totalWordCount - 1) { + cost = 0; // Last line + } else { + const int remainingSpace = pageWidth - currlen; + // Use long long for the square to prevent overflow + const long long cost_ll = static_cast(remainingSpace) * remainingSpace + dp[j + 1]; + + if (cost_ll > MAX_COST) { + cost = MAX_COST; + } else { + cost = static_cast(cost_ll); + } + } + + if (cost < dp[i]) { + dp[i] = cost; + ans[i] = j; // j is the index of the last word in this optimal line + } + } + } + + // Stores the index of the word that starts the next line (last_word_index + 1) + std::vector lineBreakIndices; + size_t currentWordIndex = 0; + constexpr size_t MAX_LINES = 1000; + + while (currentWordIndex < totalWordCount) { + if (lineBreakIndices.size() >= MAX_LINES) { + break; + } + + size_t nextBreakIndex = ans[currentWordIndex] + 1; + lineBreakIndices.push_back(nextBreakIndex); + + currentWordIndex = nextBreakIndex; + } + + std::list> lines; + + // Initialize iterators for consumption + auto wordStartIt = words.begin(); + auto wordStyleStartIt = wordStyles.begin(); + size_t wordWidthIndex = 0; + + size_t lastBreakAt = 0; + for (const size_t lineBreak : lineBreakIndices) { + const size_t lineWordCount = lineBreak - lastBreakAt; + + // Calculate end iterators for the range to splice + auto wordEndIt = wordStartIt; + auto wordStyleEndIt = wordStyleStartIt; + std::advance(wordEndIt, lineWordCount); + std::advance(wordStyleEndIt, lineWordCount); + + // Calculate total word width for this line + int lineWordWidthSum = 0; + for (size_t i = 0; i < lineWordCount; ++i) { + lineWordWidthSum += wordWidths[wordWidthIndex + i]; + } + + // Calculate spacing + const int spareSpace = pageWidth - lineWordWidthSum; + int spacing = spaceWidth; + const bool isLastLine = lineBreak == totalWordCount; + + if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) { + spacing = spareSpace / (lineWordCount - 1); + } + + // Calculate initial x position + uint16_t xpos = 0; + if (style == TextBlock::RIGHT_ALIGN) { + xpos = spareSpace - (lineWordCount - 1) * spaceWidth; + } else if (style == TextBlock::CENTER_ALIGN) { + xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2; + } + + // Pre-calculate X positions for words + std::list lineXPos; + for (size_t i = 0; i < lineWordCount; ++i) { + const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i]; + lineXPos.push_back(xpos); + xpos += currentWordWidth + spacing; + } + + // *** CRITICAL STEP: CONSUME DATA USING SPLICE *** + std::list lineWords; + lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt); + std::list lineWordStyles; + lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt); + + lines.push_back( + std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); + + // Update pointers/indices for the next line + wordStartIt = wordEndIt; + wordStyleStartIt = wordStyleEndIt; + wordWidthIndex += lineWordCount; + lastBreakAt = lineBreak; + } + + return lines; +} diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h new file mode 100644 index 0000000..05f2532 --- /dev/null +++ b/lib/Epub/Epub/ParsedText.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "blocks/TextBlock.h" + +class GfxRenderer; + +class ParsedText { + std::list words; + std::list wordStyles; + TextBlock::BLOCK_STYLE style; + + public: + explicit ParsedText(const TextBlock::BLOCK_STYLE style) : style(style) {} + ~ParsedText() = default; + + void addWord(std::string word, EpdFontStyle fontStyle); + void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; } + TextBlock::BLOCK_STYLE getStyle() const { return style; } + bool isEmpty() const { return words.empty(); } + std::list> layoutAndExtractLines(const GfxRenderer& renderer, int fontId, + int horizontalMargin); +}; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 7a6b481..b5eadf4 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -1,17 +1,17 @@ #include "Section.h" -#include #include +#include #include #include "EpubHtmlParserSlim.h" +#include "FsHelpers.h" #include "Page.h" -#include "Serialization.h" -constexpr uint8_t SECTION_FILE_VERSION = 3; +constexpr uint8_t SECTION_FILE_VERSION = 4; -void Section::onPageComplete(const Page* page) { +void Section::onPageComplete(std::unique_ptr page) { const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; std::ofstream outputFile("/sd" + filePath); @@ -21,7 +21,6 @@ void Section::onPageComplete(const Page* page) { Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount); pageCount++; - delete page; } void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop, @@ -57,8 +56,8 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c serialization::readPod(inputFile, version); if (version != SECTION_FILE_VERSION) { inputFile.close(); - clearCache(); Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version); + clearCache(); return false; } @@ -74,8 +73,8 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop || marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) { inputFile.close(); - clearCache(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); + clearCache(); return false; } } @@ -91,7 +90,21 @@ void Section::setupCacheDir() const { SD.mkdir(cachePath.c_str()); } -void Section::clearCache() const { SD.rmdir(cachePath.c_str()); } +// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) +bool Section::clearCache() const { + if (!SD.exists(cachePath.c_str())) { + Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis()); + return true; + } + + if (!FsHelpers::removeDir(cachePath.c_str())) { + Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis()); + return false; + } + + Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis()); + return true; +} bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft) { @@ -114,8 +127,9 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath; - auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, - marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); }); + EpubHtmlParserSlim visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, + marginBottom, marginLeft, + [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }); success = visitor.parseAndBuildPages(); SD.remove(tmpHtmlPath.c_str()); @@ -129,7 +143,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, return true; } -Page* Section::loadPageFromSD() const { +std::unique_ptr Section::loadPageFromSD() const { const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin"; if (!SD.exists(filePath.c_str() + 3)) { Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str()); @@ -137,7 +151,7 @@ Page* Section::loadPageFromSD() const { } std::ifstream inputFile(filePath); - Page* p = Page::deserialize(inputFile); + auto page = Page::deserialize(inputFile); inputFile.close(); - return p; + return page; } diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 036a42d..4c98fbe 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -1,24 +1,26 @@ #pragma once +#include + #include "Epub.h" class Page; class GfxRenderer; class Section { - Epub* epub; + std::shared_ptr epub; const int spineIndex; GfxRenderer& renderer; std::string cachePath; void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, int marginLeft) const; - void onPageComplete(const Page* page); + void onPageComplete(std::unique_ptr page); public: int pageCount = 0; int currentPage = 0; - explicit Section(Epub* epub, const int spineIndex, GfxRenderer& renderer) + explicit Section(const std::shared_ptr& epub, const int spineIndex, GfxRenderer& renderer) : epub(epub), spineIndex(spineIndex), renderer(renderer) { cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex); } @@ -26,8 +28,8 @@ class Section { bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, int marginLeft); void setupCacheDir() const; - void clearCache() const; + bool clearCache() const; bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, int marginLeft); - Page* loadPageFromSD() const; + std::unique_ptr loadPageFromSD() const; }; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 2b3bd49..cc3cb60 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -3,170 +3,17 @@ #include #include -void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) { - if (word.length() == 0) return; - - words.push_back(word); - wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0)); -} - -std::list TextBlock::splitIntoLines(const GfxRenderer& renderer, const int fontId, - const int horizontalMargin) { - const int totalWordCount = words.size(); - const int pageWidth = GfxRenderer::getScreenWidth() - horizontalMargin; - const int spaceWidth = renderer.getSpaceWidth(fontId); - - words.shrink_to_fit(); - wordStyles.shrink_to_fit(); - wordXpos.reserve(totalWordCount); - - // measure each word - uint16_t wordWidths[totalWordCount]; - for (int i = 0; i < totalWordCount; i++) { - // measure the word - EpdFontStyle fontStyle = REGULAR; - if (wordStyles[i] & BOLD_SPAN) { - if (wordStyles[i] & ITALIC_SPAN) { - fontStyle = BOLD_ITALIC; - } else { - fontStyle = BOLD; - } - } else if (wordStyles[i] & ITALIC_SPAN) { - fontStyle = ITALIC; - } - const int width = renderer.getTextWidth(fontId, words[i].c_str(), fontStyle); - wordWidths[i] = width; - } - - // now apply the dynamic programming algorithm to find the best line breaks - // DP table in which dp[i] represents cost of line starting with word words[i] - int dp[totalWordCount]; - - // Array in which ans[i] store index of last word in line starting with word - // word[i] - size_t ans[totalWordCount]; - - // If only one word is present then only one line is required. Cost of last - // line is zero. Hence cost of this line is zero. Ending point is also n-1 as - // single word is present - dp[totalWordCount - 1] = 0; - ans[totalWordCount - 1] = totalWordCount - 1; - - // Make each word first word of line by iterating over each index in arr. - for (int i = totalWordCount - 2; i >= 0; i--) { - int currlen = -1; - dp[i] = INT_MAX; - - // Variable to store possible minimum cost of line. - int cost; - - // Keep on adding words in current line by iterating from starting word upto - // last word in arr. - for (int j = i; j < totalWordCount; j++) { - // Update the width of the words in current line + the space between two - // words. - currlen += wordWidths[j] + spaceWidth; - - // If we're bigger than the current pagewidth then we can't add more words - if (currlen > pageWidth) break; - - // if we've run out of words then this is last line and the cost should be - // 0 Otherwise the cost is the sqaure of the left over space + the costs - // of all the previous lines - if (j == totalWordCount - 1) - cost = 0; - else - cost = (pageWidth - currlen) * (pageWidth - currlen) + dp[j + 1]; - - // Check if this arrangement gives minimum cost for line starting with - // word words[i]. - if (cost < dp[i]) { - dp[i] = cost; - ans[i] = j; - } - } - } - - // We can now iterate through the answer to find the line break positions - std::list lineBreaks; - for (size_t i = 0; i < totalWordCount;) { - i = ans[i] + 1; - if (i > totalWordCount) { - break; - } - lineBreaks.push_back(i); - // Text too big, just exit - if (lineBreaks.size() > 1000) { - break; - } - } - - std::list lines; - - // With the line breaks calculated we can now position the words along the - // line - int startWord = 0; - for (const auto lineBreak : lineBreaks) { - const int lineWordCount = lineBreak - startWord; - - int lineWordWidthSum = 0; - for (int i = startWord; i < lineBreak; i++) { - lineWordWidthSum += wordWidths[i]; - } - - // Calculate spacing between words - const uint16_t spareSpace = pageWidth - lineWordWidthSum; - uint16_t spacing = spaceWidth; - // evenly space words if using justified style, not the last line, and at - // least 2 words - if (style == JUSTIFIED && lineBreak != lineBreaks.back() && lineWordCount >= 2) { - spacing = spareSpace / (lineWordCount - 1); - } - - uint16_t xpos = 0; - if (style == RIGHT_ALIGN) { - xpos = spareSpace - (lineWordCount - 1) * spaceWidth; - } else if (style == CENTER_ALIGN) { - xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2; - } - - for (int i = startWord; i < lineBreak; i++) { - wordXpos[i] = xpos; - xpos += wordWidths[i] + spacing; - } - - std::vector lineWords; - std::vector lineXPos; - std::vector lineWordStyles; - lineWords.reserve(lineWordCount); - lineXPos.reserve(lineWordCount); - lineWordStyles.reserve(lineWordCount); - - for (int i = startWord; i < lineBreak; i++) { - lineWords.push_back(words[i]); - lineXPos.push_back(wordXpos[i]); - lineWordStyles.push_back(wordStyles[i]); - } - const auto textLine = new TextBlock(lineWords, lineXPos, lineWordStyles, style); - lines.push_back(textLine); - startWord = lineBreak; - } - - return lines; -} - void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { + auto wordIt = words.begin(); + auto wordStylesIt = wordStyles.begin(); + auto wordXposIt = wordXpos.begin(); + for (int i = 0; i < words.size(); i++) { - // render the word - EpdFontStyle fontStyle = REGULAR; - if (wordStyles[i] & BOLD_SPAN && wordStyles[i] & ITALIC_SPAN) { - fontStyle = BOLD_ITALIC; - } else if (wordStyles[i] & BOLD_SPAN) { - fontStyle = BOLD; - } else if (wordStyles[i] & ITALIC_SPAN) { - fontStyle = ITALIC; - } - renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle); + renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt); + + std::advance(wordIt, 1); + std::advance(wordStylesIt, 1); + std::advance(wordXposIt, 1); } } @@ -190,11 +37,11 @@ void TextBlock::serialize(std::ostream& os) const { serialization::writePod(os, style); } -TextBlock* TextBlock::deserialize(std::istream& is) { +std::unique_ptr TextBlock::deserialize(std::istream& is) { uint32_t wc, xc, sc; - std::vector words; - std::vector wordXpos; - std::vector wordStyles; + std::list words; + std::list wordXpos; + std::list wordStyles; BLOCK_STYLE style; // words @@ -215,5 +62,5 @@ TextBlock* TextBlock::deserialize(std::istream& is) { // style serialization::readPod(is, style); - return new TextBlock(words, wordXpos, wordStyles, style); + return std::unique_ptr(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style)); } diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index afd3178..4b2b031 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -1,50 +1,40 @@ #pragma once +#include + #include +#include #include -#include #include "Block.h" -enum SPAN_STYLE : uint8_t { - BOLD_SPAN = 1, - ITALIC_SPAN = 2, -}; - -enum BLOCK_STYLE : uint8_t { - JUSTIFIED = 0, - LEFT_ALIGN = 1, - CENTER_ALIGN = 2, - RIGHT_ALIGN = 3, -}; - // represents a block of words in the html document class TextBlock final : public Block { - // pointer to each word - std::vector words; - // x position of each word - std::vector wordXpos; - // the styles of each word - std::vector wordStyles; + public: + enum BLOCK_STYLE : uint8_t { + JUSTIFIED = 0, + LEFT_ALIGN = 1, + CENTER_ALIGN = 2, + RIGHT_ALIGN = 3, + }; - // the style of the block - left, center, right aligned + private: + std::list words; + std::list wordXpos; + std::list wordStyles; BLOCK_STYLE style; public: - explicit TextBlock(const BLOCK_STYLE style) : style(style) {} - explicit TextBlock(const std::vector& words, const std::vector& word_xpos, - // the styles of each word - const std::vector& word_styles, const BLOCK_STYLE style) - : words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {} + explicit TextBlock(std::list words, std::list word_xpos, std::list word_styles, + const BLOCK_STYLE style) + : words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {} ~TextBlock() override = default; - void addWord(const std::string& word, bool is_bold, bool is_italic); void setStyle(const BLOCK_STYLE style) { this->style = style; } BLOCK_STYLE getStyle() const { return style; } bool isEmpty() override { return words.empty(); } void layout(GfxRenderer& renderer) override {}; // given a renderer works out where to break the words into lines - std::list splitIntoLines(const GfxRenderer& renderer, int fontId, int horizontalMargin); void render(const GfxRenderer& renderer, int fontId, int x, int y) const; BlockType getType() override { return TEXT_BLOCK; } void serialize(std::ostream& os) const; - static TextBlock* deserialize(std::istream& is); + static std::unique_ptr deserialize(std::istream& is); }; diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index a927bbb..7482cb9 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -1,11 +1,10 @@ #pragma once #include +#include #include -#include "EpdFontFamily.h" - class GfxRenderer { public: enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; diff --git a/platformio.ini b/platformio.ini index c9c0c47..a74fc4e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,6 +20,7 @@ build_flags = # https://libexpat.github.io/doc/api/latest/#XML_GE -DXML_GE=0 -DXML_CONTEXT_BYTES=1024 + -std=c++2a ; Board configuration board_build.flash_mode = dio diff --git a/src/main.cpp b/src/main.cpp index c285a41..8b6fc7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -62,19 +62,18 @@ constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000; // Time required to enter sleep mode constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000; -Epub* loadEpub(const std::string& path) { +std::unique_ptr loadEpub(const std::string& path) { if (!SD.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } - const auto epub = new Epub(path, "/.crosspoint"); + auto epub = std::unique_ptr(new Epub(path, "/.crosspoint")); if (epub->load()) { return epub; } Serial.printf("[%lu] [ ] Failed to load epub\n", millis()); - delete epub; return nullptr; } @@ -151,12 +150,12 @@ void onSelectEpubFile(const std::string& path) { exitScreen(); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); - Epub* epub = loadEpub(path); + auto epub = loadEpub(path); if (epub) { appState.openEpubPath = path; appState.saveToFile(); exitScreen(); - enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome)); + enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome)); } else { exitScreen(); enterNewScreen( @@ -206,10 +205,10 @@ void setup() { appState.loadFromFile(); if (!appState.openEpubPath.empty()) { - Epub* epub = loadEpub(appState.openEpubPath); + auto epub = loadEpub(appState.openEpubPath); if (epub) { exitScreen(); - enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome)); + enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome)); // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); return; diff --git a/src/screens/EpubReaderScreen.cpp b/src/screens/EpubReaderScreen.cpp index 241fc50..818fcff 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/screens/EpubReaderScreen.cpp @@ -10,9 +10,9 @@ constexpr int PAGES_PER_REFRESH = 15; constexpr unsigned long SKIP_CHAPTER_MS = 700; constexpr float lineCompression = 0.95f; -constexpr int marginTop = 11; +constexpr int marginTop = 10; constexpr int marginRight = 10; -constexpr int marginBottom = 30; +constexpr int marginBottom = 20; constexpr int marginLeft = 10; void EpubReaderScreen::taskTrampoline(void* param) { @@ -60,10 +60,8 @@ void EpubReaderScreen::onExit() { } vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; - delete section; - section = nullptr; - delete epub; - epub = nullptr; + section.reset(); + epub.reset(); } void EpubReaderScreen::handleInput() { @@ -88,8 +86,7 @@ void EpubReaderScreen::handleInput() { xSemaphoreTake(renderingMutex, portMAX_DELAY); nextPageNumber = 0; currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1; - delete section; - section = nullptr; + section.reset(); xSemaphoreGive(renderingMutex); updateRequired = true; return; @@ -109,8 +106,7 @@ void EpubReaderScreen::handleInput() { xSemaphoreTake(renderingMutex, portMAX_DELAY); nextPageNumber = UINT16_MAX; currentSpineIndex--; - delete section; - section = nullptr; + section.reset(); xSemaphoreGive(renderingMutex); } updateRequired = true; @@ -122,8 +118,7 @@ void EpubReaderScreen::handleInput() { xSemaphoreTake(renderingMutex, portMAX_DELAY); nextPageNumber = 0; currentSpineIndex++; - delete section; - section = nullptr; + section.reset(); xSemaphoreGive(renderingMutex); } updateRequired = true; @@ -155,7 +150,7 @@ void EpubReaderScreen::renderScreen() { if (!section) { const auto filepath = epub->getSpineItem(currentSpineIndex); Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); - section = new Section(epub, currentSpineIndex, renderer); + section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); @@ -179,8 +174,7 @@ void EpubReaderScreen::renderScreen() { if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); - delete section; - section = nullptr; + section.reset(); return; } } else { @@ -212,11 +206,18 @@ void EpubReaderScreen::renderScreen() { return; } - const Page* p = section->loadPageFromSD(); - const auto start = millis(); - renderContents(p); - Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); - delete p; + { + auto p = section->loadPageFromSD(); + if (!p) { + Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis()); + section->clearCache(); + section.reset(); + return renderScreen(); + } + const auto start = millis(); + renderContents(std::move(p)); + Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); + } File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE); uint8_t data[4]; @@ -228,8 +229,8 @@ void EpubReaderScreen::renderScreen() { f.close(); } -void EpubReaderScreen::renderContents(const Page* p) { - p->render(renderer, READER_FONT_ID); +void EpubReaderScreen::renderContents(std::unique_ptr page) { + page->render(renderer, READER_FONT_ID); renderStatusBar(); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); @@ -244,13 +245,13 @@ void EpubReaderScreen::renderContents(const Page* p) { { renderer.clearScreen(0x00); renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB); - p->render(renderer, READER_FONT_ID); + page->render(renderer, READER_FONT_ID); renderer.copyGrayscaleLsbBuffers(); // Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB); - p->render(renderer, READER_FONT_ID); + page->render(renderer, READER_FONT_ID); renderer.copyGrayscaleMsbBuffers(); // display grayscale part diff --git a/src/screens/EpubReaderScreen.h b/src/screens/EpubReaderScreen.h index 2301769..5b00ad8 100644 --- a/src/screens/EpubReaderScreen.h +++ b/src/screens/EpubReaderScreen.h @@ -8,8 +8,8 @@ #include "Screen.h" class EpubReaderScreen final : public Screen { - Epub* epub; - Section* section = nullptr; + std::shared_ptr epub; + std::unique_ptr
section = nullptr; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; int currentSpineIndex = 0; @@ -21,13 +21,13 @@ class EpubReaderScreen final : public Screen { static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void renderScreen(); - void renderContents(const Page* p); + void renderContents(std::unique_ptr p); void renderStatusBar() const; public: - explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, Epub* epub, + explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr epub, const std::function& onGoHome) - : Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {} + : Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {} void onEnter() override; void onExit() override; void handleInput() override;