Move to smart pointers and split out ParsedText class (#6)

* Move to smart pointers and split out ParsedText class

* Cleanup ParsedText

* Fix clearCache functions and clear section cache if page load fails

* Bump Page and Section file versions

* Combine removeDir implementations in Epub

* Adjust screen margins
This commit is contained in:
Dave Allie 2025-12-12 22:13:34 +11:00 committed by GitHub
parent 09f68a3d03
commit 69f357998e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 430 additions and 322 deletions

2
.clangd Normal file
View File

@ -0,0 +1,2 @@
CompileFlags:
Add: [-std=c++2a]

View File

@ -6,6 +6,8 @@
#include <map> #include <map>
#include "Epub/FsHelpers.h"
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) { bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
// open up the meta data to find where the content.opf file lives // open up the meta data to find where the content.opf file lives
size_t s; size_t s;
@ -249,7 +251,20 @@ bool Epub::load() {
return true; 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 { void Epub::setupCacheDir() const {
if (SD.exists(cachePath.c_str())) { if (SD.exists(cachePath.c_str())) {

View File

@ -50,7 +50,7 @@ class Epub {
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
bool load(); bool load();
void clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
const std::string& getCachePath() const; const std::string& getCachePath() const;
const std::string& getPath() const; const std::string& getPath() const;

View File

@ -38,7 +38,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
} }
// start a new text block if needed // start a new text block if needed
void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) { void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
if (currentTextBlock) { if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it // already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) { if (currentTextBlock->isEmpty()) {
@ -46,11 +46,9 @@ void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
return; return;
} }
currentTextBlock->finish();
makePages(); 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) { 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)) { if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(CENTER_ALIGN); self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->boldUntilDepth = min(self->boldUntilDepth, self->depth); self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
if (strcmp(name, "br") == 0) { if (strcmp(name, "br") == 0) {
self->startNewTextBlock(self->currentTextBlock->getStyle()); self->startNewTextBlock(self->currentTextBlock->getStyle());
} else { } else {
self->startNewTextBlock(JUSTIFIED); self->startNewTextBlock(TextBlock::JUSTIFIED);
} }
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { } else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = min(self->boldUntilDepth, self->depth); self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
@ -119,13 +117,21 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
return; 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++) { for (int i = 0; i < len; i++) {
if (isWhitespace(s[i])) { if (isWhitespace(s[i])) {
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it // Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
if (self->partWordBufferIndex > 0) { if (self->partWordBufferIndex > 0) {
self->partWordBuffer[self->partWordBufferIndex] = '\0'; self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0; self->partWordBufferIndex = 0;
} }
// Skip the whitespace char // 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 we're about to run out of space, then cut the word off and start a new one
if (self->partWordBufferIndex >= MAX_WORD_SIZE) { if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
self->partWordBuffer[self->partWordBufferIndex] = '\0'; self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0; 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; matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
if (shouldBreakText) { 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->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0; self->partWordBufferIndex = 0;
} }
} }
@ -184,7 +197,7 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
} }
bool EpubHtmlParserSlim::parseAndBuildPages() { bool EpubHtmlParserSlim::parseAndBuildPages() {
startNewTextBlock(JUSTIFIED); startNewTextBlock(TextBlock::JUSTIFIED);
const XML_Parser parser = XML_ParserCreate(nullptr); const XML_Parser parser = XML_ParserCreate(nullptr);
int done; int done;
@ -240,10 +253,9 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
// Process last page if there is still text // Process last page if there is still text
if (currentTextBlock) { if (currentTextBlock) {
makePages(); makePages();
completePageFn(currentPage); completePageFn(std::move(currentPage));
currentPage = nullptr; currentPage.reset();
delete currentTextBlock; currentTextBlock.reset();
currentTextBlock = nullptr;
} }
return true; return true;
@ -256,7 +268,7 @@ void EpubHtmlParserSlim::makePages() {
} }
if (!currentPage) { if (!currentPage) {
currentPage = new Page(); currentPage.reset(new Page());
currentPageNextY = marginTop; currentPageNextY = marginTop;
} }
@ -266,30 +278,18 @@ void EpubHtmlParserSlim::makePages() {
// Long running task, make sure to let other things happen // Long running task, make sure to let other things happen
vTaskDelay(1); vTaskDelay(1);
if (currentTextBlock->getType() == TEXT_BLOCK) { const auto lines = currentTextBlock->layoutAndExtractLines(renderer, fontId, marginLeft + marginRight);
const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight);
for (const auto line : lines) { for (auto&& line : lines) {
if (currentPageNextY + lineHeight > pageHeight) { if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(currentPage); completePageFn(std::move(currentPage));
currentPage = new Page(); currentPage.reset(new Page());
currentPageNextY = marginTop; currentPageNextY = marginTop;
}
currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
} }
// add some extra line between blocks
currentPageNextY += lineHeight / 2; currentPage->elements.push_back(std::make_shared<PageLine>(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
} }
// TODO: Image block support // add some extra line between blocks
// if (block->getType() == BlockType::IMAGE_BLOCK) { currentPageNextY += lineHeight / 2;
// 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;
// }
} }

View File

@ -4,7 +4,9 @@
#include <climits> #include <climits>
#include <functional> #include <functional>
#include <memory>
#include "ParsedText.h"
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
class Page; class Page;
@ -15,7 +17,7 @@ class GfxRenderer;
class EpubHtmlParserSlim { class EpubHtmlParserSlim {
const char* filepath; const char* filepath;
GfxRenderer& renderer; GfxRenderer& renderer;
std::function<void(Page*)> completePageFn; std::function<void(std::unique_ptr<Page>)> completePageFn;
int depth = 0; int depth = 0;
int skipUntilDepth = INT_MAX; int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX;
@ -24,8 +26,8 @@ class EpubHtmlParserSlim {
// leave one char at end for null pointer // leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {}; char partWordBuffer[MAX_WORD_SIZE + 1] = {};
int partWordBufferIndex = 0; int partWordBufferIndex = 0;
TextBlock* currentTextBlock = nullptr; std::unique_ptr<ParsedText> currentTextBlock = nullptr;
Page* currentPage = nullptr; std::unique_ptr<Page> currentPage = nullptr;
int currentPageNextY = 0; int currentPageNextY = 0;
int fontId; int fontId;
float lineCompression; float lineCompression;
@ -34,7 +36,7 @@ class EpubHtmlParserSlim {
int marginBottom; int marginBottom;
int marginLeft; int marginLeft;
void startNewTextBlock(BLOCK_STYLE style); void startNewTextBlock(TextBlock::BLOCK_STYLE style);
void makePages(); void makePages();
// XML callbacks // XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); 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, explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId,
const float lineCompression, const int marginTop, const int marginRight, const float lineCompression, const int marginTop, const int marginRight,
const int marginBottom, const int marginLeft, const int marginBottom, const int marginLeft,
const std::function<void(Page*)>& completePageFn) const std::function<void(std::unique_ptr<Page>)>& completePageFn)
: filepath(filepath), : filepath(filepath),
renderer(renderer), renderer(renderer),
fontId(fontId), fontId(fontId),

View File

@ -0,0 +1,36 @@
#include "FsHelpers.h"
#include <SD.h>
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);
}

View File

@ -0,0 +1,6 @@
#pragma once
class FsHelpers {
public:
static bool removeDir(const char* path);
};

View File

@ -3,7 +3,7 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
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); } 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); block->serialize(os);
} }
PageLine* PageLine::deserialize(std::istream& is) { std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
int32_t xPos; int32_t xPos;
int32_t yPos; int32_t yPos;
serialization::readPod(is, xPos); serialization::readPod(is, xPos);
serialization::readPod(is, yPos); serialization::readPod(is, yPos);
const auto tb = TextBlock::deserialize(is); auto tb = TextBlock::deserialize(is);
return new PageLine(tb, xPos, yPos); return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
} }
void Page::render(GfxRenderer& renderer, const int fontId) const { 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(); const uint32_t count = elements.size();
serialization::writePod(os, count); serialization::writePod(os, count);
for (auto* el : elements) { for (const auto& el : elements) {
// Only PageLine exists currently // Only PageLine exists currently
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine)); serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
static_cast<PageLine*>(el)->serialize(os); el->serialize(os);
} }
} }
Page* Page::deserialize(std::istream& is) { std::unique_ptr<Page> Page::deserialize(std::istream& is) {
uint8_t version; uint8_t version;
serialization::readPod(is, version); serialization::readPod(is, version);
if (version != PAGE_FILE_VERSION) { if (version != PAGE_FILE_VERSION) {
@ -52,7 +52,7 @@ Page* Page::deserialize(std::istream& is) {
return nullptr; return nullptr;
} }
auto* page = new Page(); auto page = std::unique_ptr<Page>(new Page());
uint32_t count; uint32_t count;
serialization::readPod(is, count); serialization::readPod(is, count);
@ -62,10 +62,11 @@ Page* Page::deserialize(std::istream& is) {
serialization::readPod(is, tag); serialization::readPod(is, tag);
if (tag == TAG_PageLine) { if (tag == TAG_PageLine) {
auto* pl = PageLine::deserialize(is); auto pl = PageLine::deserialize(is);
page->elements.push_back(pl); page->elements.push_back(std::move(pl));
} else { } else {
throw std::runtime_error("Unknown PageElement tag"); Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
return nullptr;
} }
} }

View File

@ -1,4 +1,7 @@
#pragma once #pragma once
#include <utility>
#include <vector>
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
enum PageElementTag : uint8_t { enum PageElementTag : uint8_t {
@ -18,27 +21,21 @@ class PageElement {
// a line from a block element // a line from a block element
class PageLine final : public PageElement { class PageLine final : public PageElement {
const TextBlock* block; std::shared_ptr<TextBlock> block;
public: public:
PageLine(const TextBlock* block, const int xPos, const int yPos) : PageElement(xPos, yPos), block(block) {} PageLine(std::shared_ptr<TextBlock> block, const int xPos, const int yPos)
~PageLine() override { delete block; } : PageElement(xPos, yPos), block(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId) override; void render(GfxRenderer& renderer, int fontId) override;
void serialize(std::ostream& os) override; void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is); static std::unique_ptr<PageLine> deserialize(std::istream& is);
}; };
class Page { class Page {
public: public:
~Page() {
for (const auto element : elements) {
delete element;
}
}
// the list of block index and line numbers on this page // the list of block index and line numbers on this page
std::vector<PageElement*> elements; std::vector<std::shared_ptr<PageElement>> elements;
void render(GfxRenderer& renderer, int fontId) const; void render(GfxRenderer& renderer, int fontId) const;
void serialize(std::ostream& os) const; void serialize(std::ostream& os) const;
static Page* deserialize(std::istream& is); static std::unique_ptr<Page> deserialize(std::istream& is);
}; };

View File

@ -0,0 +1,167 @@
#include "ParsedText.h"
#include <GfxRenderer.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
constexpr int MAX_COST = std::numeric_limits<int>::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<std::shared_ptr<TextBlock>> 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<uint16_t> 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<int> dp(totalWordCount);
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
std::vector<size_t> 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<long long>(remainingSpace) * remainingSpace + dp[j + 1];
if (cost_ll > MAX_COST) {
cost = MAX_COST;
} else {
cost = static_cast<int>(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<size_t> 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<std::shared_ptr<TextBlock>> 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<uint16_t> 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<std::string> lineWords;
lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt);
std::list<EpdFontStyle> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt);
lines.push_back(
std::make_shared<TextBlock>(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;
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <EpdFontFamily.h>
#include <cstdint>
#include <list>
#include <memory>
#include <string>
#include "blocks/TextBlock.h"
class GfxRenderer;
class ParsedText {
std::list<std::string> words;
std::list<EpdFontStyle> 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<std::shared_ptr<TextBlock>> layoutAndExtractLines(const GfxRenderer& renderer, int fontId,
int horizontalMargin);
};

View File

@ -1,17 +1,17 @@
#include "Section.h" #include "Section.h"
#include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
#include <Serialization.h>
#include <fstream> #include <fstream>
#include "EpubHtmlParserSlim.h" #include "EpubHtmlParserSlim.h"
#include "FsHelpers.h"
#include "Page.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> page) {
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
std::ofstream outputFile("/sd" + filePath); 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); Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
pageCount++; pageCount++;
delete page;
} }
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop, 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); serialization::readPod(inputFile, version);
if (version != SECTION_FILE_VERSION) { if (version != SECTION_FILE_VERSION) {
inputFile.close(); inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version); Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
clearCache();
return false; return false;
} }
@ -74,8 +73,8 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop || if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) { marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
inputFile.close(); inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
clearCache();
return false; return false;
} }
} }
@ -91,7 +90,21 @@ void Section::setupCacheDir() const {
SD.mkdir(cachePath.c_str()); 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, bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) { const int marginRight, const int marginBottom, const int marginLeft) {
@ -114,8 +127,9 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath; const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, EpubHtmlParserSlim visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); }); marginBottom, marginLeft,
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();
SD.remove(tmpHtmlPath.c_str()); SD.remove(tmpHtmlPath.c_str());
@ -129,7 +143,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
return true; return true;
} }
Page* Section::loadPageFromSD() const { std::unique_ptr<Page> Section::loadPageFromSD() const {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin"; const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
if (!SD.exists(filePath.c_str() + 3)) { if (!SD.exists(filePath.c_str() + 3)) {
Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str()); 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); std::ifstream inputFile(filePath);
Page* p = Page::deserialize(inputFile); auto page = Page::deserialize(inputFile);
inputFile.close(); inputFile.close();
return p; return page;
} }

View File

@ -1,24 +1,26 @@
#pragma once #pragma once
#include <memory>
#include "Epub.h" #include "Epub.h"
class Page; class Page;
class GfxRenderer; class GfxRenderer;
class Section { class Section {
Epub* epub; std::shared_ptr<Epub> epub;
const int spineIndex; const int spineIndex;
GfxRenderer& renderer; GfxRenderer& renderer;
std::string cachePath; std::string cachePath;
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft) const; int marginLeft) const;
void onPageComplete(const Page* page); void onPageComplete(std::unique_ptr<Page> page);
public: public:
int pageCount = 0; int pageCount = 0;
int currentPage = 0; int currentPage = 0;
explicit Section(Epub* epub, const int spineIndex, GfxRenderer& renderer) explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub), spineIndex(spineIndex), renderer(renderer) { : epub(epub), spineIndex(spineIndex), renderer(renderer) {
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex); 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, bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft); int marginLeft);
void setupCacheDir() const; void setupCacheDir() const;
void clearCache() const; bool clearCache() const;
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft); int marginLeft);
Page* loadPageFromSD() const; std::unique_ptr<Page> loadPageFromSD() const;
}; };

View File

@ -3,170 +3,17 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <Serialization.h> #include <Serialization.h>
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*> 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<uint16_t> 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<TextBlock*> 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<std::string> lineWords;
std::vector<uint16_t> lineXPos;
std::vector<uint8_t> 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 { 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++) { for (int i = 0; i < words.size(); i++) {
// render the word renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN && wordStyles[i] & ITALIC_SPAN) { std::advance(wordIt, 1);
fontStyle = BOLD_ITALIC; std::advance(wordStylesIt, 1);
} else if (wordStyles[i] & BOLD_SPAN) { std::advance(wordXposIt, 1);
fontStyle = BOLD;
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
} }
} }
@ -190,11 +37,11 @@ void TextBlock::serialize(std::ostream& os) const {
serialization::writePod(os, style); serialization::writePod(os, style);
} }
TextBlock* TextBlock::deserialize(std::istream& is) { std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
uint32_t wc, xc, sc; uint32_t wc, xc, sc;
std::vector<std::string> words; std::list<std::string> words;
std::vector<uint16_t> wordXpos; std::list<uint16_t> wordXpos;
std::vector<uint8_t> wordStyles; std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style; BLOCK_STYLE style;
// words // words
@ -215,5 +62,5 @@ TextBlock* TextBlock::deserialize(std::istream& is) {
// style // style
serialization::readPod(is, style); serialization::readPod(is, style);
return new TextBlock(words, wordXpos, wordStyles, style); return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
} }

View File

@ -1,50 +1,40 @@
#pragma once #pragma once
#include <EpdFontFamily.h>
#include <list> #include <list>
#include <memory>
#include <string> #include <string>
#include <vector>
#include "Block.h" #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 // represents a block of words in the html document
class TextBlock final : public Block { class TextBlock final : public Block {
// pointer to each word public:
std::vector<std::string> words; enum BLOCK_STYLE : uint8_t {
// x position of each word JUSTIFIED = 0,
std::vector<uint16_t> wordXpos; LEFT_ALIGN = 1,
// the styles of each word CENTER_ALIGN = 2,
std::vector<uint8_t> wordStyles; RIGHT_ALIGN = 3,
};
// the style of the block - left, center, right aligned private:
std::list<std::string> words;
std::list<uint16_t> wordXpos;
std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style; BLOCK_STYLE style;
public: public:
explicit TextBlock(const BLOCK_STYLE style) : style(style) {} explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos, const BLOCK_STYLE style)
// the styles of each word : words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
~TextBlock() override = default; ~TextBlock() override = default;
void addWord(const std::string& word, bool is_bold, bool is_italic);
void setStyle(const BLOCK_STYLE style) { this->style = style; } void setStyle(const BLOCK_STYLE style) { this->style = style; }
BLOCK_STYLE getStyle() const { return style; } BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() override { return words.empty(); } bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {}; void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines // given a renderer works out where to break the words into lines
std::list<TextBlock*> splitIntoLines(const GfxRenderer& renderer, int fontId, int horizontalMargin);
void render(const GfxRenderer& renderer, int fontId, int x, int y) const; void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
BlockType getType() override { return TEXT_BLOCK; } BlockType getType() override { return TEXT_BLOCK; }
void serialize(std::ostream& os) const; void serialize(std::ostream& os) const;
static TextBlock* deserialize(std::istream& is); static std::unique_ptr<TextBlock> deserialize(std::istream& is);
}; };

View File

@ -1,11 +1,10 @@
#pragma once #pragma once
#include <EInkDisplay.h> #include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <map> #include <map>
#include "EpdFontFamily.h"
class GfxRenderer { class GfxRenderer {
public: public:
enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };

View File

@ -20,6 +20,7 @@ build_flags =
# https://libexpat.github.io/doc/api/latest/#XML_GE # https://libexpat.github.io/doc/api/latest/#XML_GE
-DXML_GE=0 -DXML_GE=0
-DXML_CONTEXT_BYTES=1024 -DXML_CONTEXT_BYTES=1024
-std=c++2a
; Board configuration ; Board configuration
board_build.flash_mode = dio board_build.flash_mode = dio

View File

@ -62,19 +62,18 @@ constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
// Time required to enter sleep mode // Time required to enter sleep mode
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000; constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
Epub* loadEpub(const std::string& path) { std::unique_ptr<Epub> loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) { if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr; return nullptr;
} }
const auto epub = new Epub(path, "/.crosspoint"); auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) { if (epub->load()) {
return epub; return epub;
} }
Serial.printf("[%lu] [ ] Failed to load epub\n", millis()); Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
delete epub;
return nullptr; return nullptr;
} }
@ -151,12 +150,12 @@ void onSelectEpubFile(const std::string& path) {
exitScreen(); exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
Epub* epub = loadEpub(path); auto epub = loadEpub(path);
if (epub) { if (epub) {
appState.openEpubPath = path; appState.openEpubPath = path;
appState.saveToFile(); appState.saveToFile();
exitScreen(); exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
} else { } else {
exitScreen(); exitScreen();
enterNewScreen( enterNewScreen(
@ -206,10 +205,10 @@ void setup() {
appState.loadFromFile(); appState.loadFromFile();
if (!appState.openEpubPath.empty()) { if (!appState.openEpubPath.empty()) {
Epub* epub = loadEpub(appState.openEpubPath); auto epub = loadEpub(appState.openEpubPath);
if (epub) { if (epub) {
exitScreen(); 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 // Ensure we're not still holding the power button before leaving setup
waitForPowerRelease(); waitForPowerRelease();
return; return;

View File

@ -10,9 +10,9 @@
constexpr int PAGES_PER_REFRESH = 15; constexpr int PAGES_PER_REFRESH = 15;
constexpr unsigned long SKIP_CHAPTER_MS = 700; constexpr unsigned long SKIP_CHAPTER_MS = 700;
constexpr float lineCompression = 0.95f; constexpr float lineCompression = 0.95f;
constexpr int marginTop = 11; constexpr int marginTop = 10;
constexpr int marginRight = 10; constexpr int marginRight = 10;
constexpr int marginBottom = 30; constexpr int marginBottom = 20;
constexpr int marginLeft = 10; constexpr int marginLeft = 10;
void EpubReaderScreen::taskTrampoline(void* param) { void EpubReaderScreen::taskTrampoline(void* param) {
@ -60,10 +60,8 @@ void EpubReaderScreen::onExit() {
} }
vSemaphoreDelete(renderingMutex); vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr; renderingMutex = nullptr;
delete section; section.reset();
section = nullptr; epub.reset();
delete epub;
epub = nullptr;
} }
void EpubReaderScreen::handleInput() { void EpubReaderScreen::handleInput() {
@ -88,8 +86,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0; nextPageNumber = 0;
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1; currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
delete section; section.reset();
section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
updateRequired = true; updateRequired = true;
return; return;
@ -109,8 +106,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX; nextPageNumber = UINT16_MAX;
currentSpineIndex--; currentSpineIndex--;
delete section; section.reset();
section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
updateRequired = true; updateRequired = true;
@ -122,8 +118,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0; nextPageNumber = 0;
currentSpineIndex++; currentSpineIndex++;
delete section; section.reset();
section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
updateRequired = true; updateRequired = true;
@ -155,7 +150,7 @@ void EpubReaderScreen::renderScreen() {
if (!section) { if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex); const auto filepath = epub->getSpineItem(currentSpineIndex);
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), 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<Section>(new Section(epub, currentSpineIndex, renderer));
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft)) { marginLeft)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); 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, if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft)) { marginLeft)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
delete section; section.reset();
section = nullptr;
return; return;
} }
} else { } else {
@ -212,11 +206,18 @@ void EpubReaderScreen::renderScreen() {
return; return;
} }
const Page* p = section->loadPageFromSD(); {
const auto start = millis(); auto p = section->loadPageFromSD();
renderContents(p); if (!p) {
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
delete p; 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); File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4]; uint8_t data[4];
@ -228,8 +229,8 @@ void EpubReaderScreen::renderScreen() {
f.close(); f.close();
} }
void EpubReaderScreen::renderContents(const Page* p) { void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
p->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderStatusBar(); renderStatusBar();
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
@ -244,13 +245,13 @@ void EpubReaderScreen::renderContents(const Page* p) {
{ {
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB); renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB);
p->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleLsbBuffers(); renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer // Render and copy to MSB buffer
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB); renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB);
p->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleMsbBuffers(); renderer.copyGrayscaleMsbBuffers();
// display grayscale part // display grayscale part

View File

@ -8,8 +8,8 @@
#include "Screen.h" #include "Screen.h"
class EpubReaderScreen final : public Screen { class EpubReaderScreen final : public Screen {
Epub* epub; std::shared_ptr<Epub> epub;
Section* section = nullptr; std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
@ -21,13 +21,13 @@ class EpubReaderScreen final : public Screen {
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void renderScreen(); void renderScreen();
void renderContents(const Page* p); void renderContents(std::unique_ptr<Page> p);
void renderStatusBar() const; void renderStatusBar() const;
public: public:
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, Epub* epub, explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoHome) const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {} : Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void handleInput() override;