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/EpubHtmlParserSlim.cpp b/lib/Epub/Epub/EpubHtmlParserSlim.cpp index 3b0db63..420e8a8 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); @@ -124,8 +122,8 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s // 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)), + self->boldUntilDepth < self->depth, self->italicUntilDepth < self->depth); self->partWordBufferIndex = 0; } // Skip the whitespace char @@ -135,8 +133,8 @@ 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)), + self->boldUntilDepth < self->depth, self->italicUntilDepth < self->depth); self->partWordBufferIndex = 0; } @@ -159,8 +157,8 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name if (shouldBreakText) { 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)), + self->boldUntilDepth < self->depth, self->italicUntilDepth < self->depth); self->partWordBufferIndex = 0; } } @@ -184,7 +182,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 +238,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 +253,7 @@ void EpubHtmlParserSlim::makePages() { } if (!currentPage) { - currentPage = new Page(); + currentPage.reset(new Page()); currentPageNextY = marginTop; } @@ -266,30 +263,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->splitIntoLines(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/Page.cpp b/lib/Epub/Epub/Page.cpp index 5f80047..d0f8e71 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -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..f8d9b9b --- /dev/null +++ b/lib/Epub/Epub/ParsedText.cpp @@ -0,0 +1,171 @@ +#include "ParsedText.h" + +#include +#include + +#include + +void ParsedText::addWord(std::string word, const bool is_bold, const bool is_italic) { + if (word.length() == 0) return; + + words.push_back(std::move(word)); + wordStyles.push_back((is_bold ? TextBlock::BOLD_SPAN : 0) | (is_italic ? TextBlock::ITALIC_SPAN : 0)); +} + +// Consumes data +std::list> ParsedText::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); + + // measure each word + std::vector wordWidths; + { + auto wordsIt = words.begin(); + auto wordStylesIt = wordStyles.begin(); + while (wordsIt != words.end() && wordStylesIt != wordStyles.end()) { + // measure the word + EpdFontStyle fontStyle = REGULAR; + if (*wordStylesIt & TextBlock::BOLD_SPAN) { + if (*wordStylesIt & TextBlock::ITALIC_SPAN) { + fontStyle = BOLD_ITALIC; + } else { + fontStyle = BOLD; + } + } else if (*wordStylesIt & TextBlock::ITALIC_SPAN) { + fontStyle = ITALIC; + } + const int width = renderer.getTextWidth(fontId, wordsIt->c_str(), fontStyle); + wordWidths.push_back(width); + std::advance(wordsIt, 1); + std::advance(wordStylesIt, 1); + } + } + + // Array in which ans[i] store index of last word in line starting with word + // word[i] + size_t ans[totalWordCount]; + { + // 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]; + + // 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 + auto wordStartIt = words.begin(); + auto wordStyleStartIt = wordStyles.begin(); + auto wordWidthStartIt = wordWidths.begin(); + uint16_t lastBreakAt = 0; + for (const auto lineBreak : lineBreaks) { + const int lineWordCount = lineBreak - lastBreakAt; + + auto wordEndIt = wordStartIt; + auto wordStyleEndIt = wordStyleStartIt; + auto wordWidthEndIt = wordWidthStartIt; + std::advance(wordEndIt, lineWordCount); + std::advance(wordStyleEndIt, lineWordCount); + std::advance(wordWidthEndIt, lineWordCount); + + int lineWordWidthSum = 0; + for (auto it = wordWidthStartIt; it != wordWidthEndIt; std::advance(it, 1)) { + lineWordWidthSum += *it; + } + + // 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 == TextBlock::JUSTIFIED && lineBreak != lineBreaks.back() && lineWordCount >= 2) { + spacing = spareSpace / (lineWordCount - 1); + } + + 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; + } + + std::list lineXPos; + + for (auto it = wordWidthStartIt; it != wordWidthEndIt; std::advance(it, 1)) { + lineXPos.push_back(xpos); + xpos += *it + spacing; + } + + std::list lineWords; + std::list lineWordStyles; + lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt); + lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt); + + lines.push_back( + std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); + + wordStartIt = wordEndIt; + wordStyleStartIt = wordStyleEndIt; + wordWidthStartIt = wordWidthEndIt; + lastBreakAt = lineBreak; + } + + return lines; +} diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h new file mode 100644 index 0000000..13fe815 --- /dev/null +++ b/lib/Epub/Epub/ParsedText.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include + +#include "blocks/TextBlock.h" + +class ParsedText { + std::list words; + std::list wordStyles; + + // the style of the block - left, center, right aligned + TextBlock::BLOCK_STYLE style; + + public: + explicit ParsedText(const TextBlock::BLOCK_STYLE style) : style(style) {} + explicit ParsedText(std::list words, std::list word_styles, const TextBlock::BLOCK_STYLE style) + : words(std::move(words)), wordStyles(std::move(word_styles)), style(style) {} + ~ParsedText() = default; + void addWord(std::string word, bool is_bold, bool is_italic); + 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> splitIntoLines(const GfxRenderer& renderer, int fontId, int horizontalMargin); +}; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 7a6b481..26c668a 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -1,6 +1,5 @@ #include "Section.h" -#include #include #include @@ -11,7 +10,7 @@ constexpr uint8_t SECTION_FILE_VERSION = 3; -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 +20,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, @@ -114,8 +112,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 +128,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 +136,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..6885d00 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); } @@ -29,5 +31,5 @@ class Section { void 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..42d5d2e 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -3,170 +3,33 @@ #include #include -void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) { +void TextBlock::addWord(std::string word, const bool is_bold, const bool is_italic) { if (word.length() == 0) return; - words.push_back(word); + words.push_back(std::move(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) { + if (*wordStylesIt & BOLD_SPAN && *wordStylesIt & ITALIC_SPAN) { fontStyle = BOLD_ITALIC; - } else if (wordStyles[i] & BOLD_SPAN) { + } else if (*wordStylesIt & BOLD_SPAN) { fontStyle = BOLD; - } else if (wordStyles[i] & ITALIC_SPAN) { + } else if (*wordStylesIt & 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, fontStyle); + + std::advance(wordIt, 1); + std::advance(wordStylesIt, 1); + std::advance(wordXposIt, 1); } } @@ -190,11 +53,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 +78,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..b5a4921 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -1,50 +1,48 @@ #pragma once #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 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, + }; + + private: + std::list words; + std::list wordXpos; + std::list wordStyles; // the style of the block - left, center, right aligned BLOCK_STYLE style; public: explicit TextBlock(const BLOCK_STYLE style) : style(style) {} - explicit TextBlock(const std::vector& words, const std::vector& word_xpos, + explicit TextBlock(std::list words, std::list 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) {} + 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 addWord(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/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..91b8a93 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/screens/EpubReaderScreen.cpp @@ -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,12 @@ 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(); + 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 +223,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 +239,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;