Remove EpdRenderer and create new GfxRenderer

This commit is contained in:
Dave Allie
2025-12-08 22:06:09 +11:00
parent 2ed8017aa2
commit b743a1ca8e
27 changed files with 564 additions and 590 deletions

View File

@@ -1,6 +1,6 @@
#include "EpubHtmlParserSlim.h"
#include <EpdRenderer.h>
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <expat.h>
@@ -133,7 +133,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 >= PART_WORD_BUFFER_SIZE - 2) {
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);
@@ -257,28 +257,30 @@ void EpubHtmlParserSlim::makePages() {
if (!currentPage) {
currentPage = new Page();
currentPageNextY = marginTop;
}
const int lineHeight = renderer.getLineHeight();
const int pageHeight = renderer.getPageHeight();
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom;
// Long running task, make sure to let other things happen
vTaskDelay(1);
if (currentTextBlock->getType() == TEXT_BLOCK) {
const auto lines = currentTextBlock->splitIntoLines(renderer);
const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight);
for (const auto line : lines) {
if (currentPage->nextY + lineHeight > pageHeight) {
if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(currentPage);
currentPage = new Page();
currentPageNextY = marginTop;
}
currentPage->elements.push_back(new PageLine(line, currentPage->nextY));
currentPage->nextY += lineHeight;
currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
}
// add some extra line between blocks
currentPage->nextY += lineHeight / 2;
currentPageNextY += lineHeight / 2;
}
// TODO: Image block support
// if (block->getType() == BlockType::IMAGE_BLOCK) {

View File

@@ -1,30 +1,38 @@
#pragma once
#include <expat.h>
#include <limits.h>
#include <climits>
#include <functional>
#include "blocks/TextBlock.h"
class Page;
class EpdRenderer;
class GfxRenderer;
#define PART_WORD_BUFFER_SIZE 200
#define MAX_WORD_SIZE 200
class EpubHtmlParserSlim {
const char* filepath;
EpdRenderer& renderer;
GfxRenderer& renderer;
std::function<void(Page*)> completePageFn;
int depth = 0;
int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX;
// If we encounter words longer than this, but this is pretty large
char partWordBuffer[PART_WORD_BUFFER_SIZE] = {};
// buffer for building up words from characters, will auto break if longer than this
// leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
int partWordBufferIndex = 0;
TextBlock* currentTextBlock = nullptr;
Page* currentPage = nullptr;
int currentPageNextY = 0;
int fontId;
float lineCompression;
int marginTop;
int marginRight;
int marginBottom;
int marginLeft;
void startNewTextBlock(BLOCK_STYLE style);
void makePages();
@@ -34,9 +42,19 @@ class EpubHtmlParserSlim {
static void XMLCALL endElement(void* userData, const XML_Char* name);
public:
explicit EpubHtmlParserSlim(const char* filepath, EpdRenderer& renderer,
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<void(Page*)>& completePageFn)
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
: filepath(filepath),
renderer(renderer),
fontId(fontId),
lineCompression(lineCompression),
marginTop(marginTop),
marginRight(marginRight),
marginBottom(marginBottom),
marginLeft(marginLeft),
completePageFn(completePageFn) {}
~EpubHtmlParserSlim() = default;
bool parseAndBuildPages();
};

View File

@@ -3,9 +3,12 @@
#include <HardwareSerial.h>
#include <Serialization.h>
void PageLine::render(EpdRenderer& renderer) { block->render(renderer, 0, yPos); }
constexpr uint8_t PAGE_FILE_VERSION = 1;
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
void PageLine::serialize(std::ostream& os) {
serialization::writePod(os, xPos);
serialization::writePod(os, yPos);
// serialize TextBlock pointed to by PageLine
@@ -13,23 +16,25 @@ void PageLine::serialize(std::ostream& os) {
}
PageLine* 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, yPos);
return new PageLine(tb, xPos, yPos);
}
void Page::render(EpdRenderer& renderer) const {
void Page::render(GfxRenderer& renderer, const int fontId) const {
const auto start = millis();
for (const auto element : elements) {
element->render(renderer);
element->render(renderer, fontId);
}
Serial.printf("Rendered page elements (%u) in %dms\n", elements.size(), millis() - start);
}
void Page::serialize(std::ostream& os) const {
serialization::writePod(os, nextY);
serialization::writePod(os, PAGE_FILE_VERSION);
const uint32_t count = elements.size();
serialization::writePod(os, count);
@@ -42,9 +47,14 @@ void Page::serialize(std::ostream& os) const {
}
Page* Page::deserialize(std::istream& is) {
auto* page = new Page();
uint8_t version;
serialization::readPod(is, version);
if (version != PAGE_FILE_VERSION) {
Serial.printf("Page: Unknown version %u\n", version);
return nullptr;
}
serialization::readPod(is, page->nextY);
auto* page = new Page();
uint32_t count;
serialization::readPod(is, count);

View File

@@ -8,10 +8,11 @@ enum PageElementTag : uint8_t {
// represents something that has been added to a page
class PageElement {
public:
int xPos;
int yPos;
explicit PageElement(const int yPos) : yPos(yPos) {}
explicit PageElement(const int xPos, const int yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default;
virtual void render(EpdRenderer& renderer) = 0;
virtual void render(GfxRenderer& renderer, int fontId) = 0;
virtual void serialize(std::ostream& os) = 0;
};
@@ -20,24 +21,24 @@ class PageLine final : public PageElement {
const TextBlock* block;
public:
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
PageLine(const TextBlock* block, const int xPos, const int yPos) : PageElement(xPos, yPos), block(block) {}
~PageLine() override { delete block; }
void render(EpdRenderer& renderer) override;
void render(GfxRenderer& renderer, int fontId) override;
void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is);
};
class Page {
public:
int nextY = 0;
// the list of block index and line numbers on this page
std::vector<PageElement*> elements;
void render(EpdRenderer& renderer) const;
~Page() {
for (const auto element : elements) {
delete element;
}
}
// the list of block index and line numbers on this page
std::vector<PageElement*> elements;
void render(GfxRenderer& renderer, int fontId) const;
void serialize(std::ostream& os) const;
static Page* deserialize(std::istream& is);
};

View File

@@ -1,6 +1,6 @@
#include "Section.h"
#include <EpdRenderer.h>
#include <GfxRenderer.h>
#include <SD.h>
#include <fstream>
@@ -9,7 +9,7 @@
#include "Page.h"
#include "Serialization.h"
constexpr uint8_t SECTION_FILE_VERSION = 2;
constexpr uint8_t SECTION_FILE_VERSION = 3;
void Section::onPageComplete(const Page* page) {
Serial.printf("Page %d complete - free mem: %lu\n", pageCount, ESP.getFreeHeap());
@@ -24,14 +24,22 @@ void Section::onPageComplete(const Page* page) {
delete page;
}
void Section::writeCacheMetadata() const {
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) const {
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
serialization::writePod(outputFile, SECTION_FILE_VERSION);
serialization::writePod(outputFile, fontId);
serialization::writePod(outputFile, lineCompression);
serialization::writePod(outputFile, marginTop);
serialization::writePod(outputFile, marginRight);
serialization::writePod(outputFile, marginBottom);
serialization::writePod(outputFile, marginLeft);
serialization::writePod(outputFile, pageCount);
outputFile.close();
}
bool Section::loadCacheMetadata() {
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) {
if (!SD.exists(cachePath.c_str())) {
return false;
}
@@ -42,14 +50,36 @@ bool Section::loadCacheMetadata() {
}
std::ifstream inputFile(("/sd" + sectionFilePath).c_str());
uint8_t version;
serialization::readPod(inputFile, version);
if (version != SECTION_FILE_VERSION) {
inputFile.close();
SD.remove(sectionFilePath.c_str());
Serial.printf("Section state file: Unknown version %u\n", version);
return false;
// Match parameters
{
uint8_t version;
serialization::readPod(inputFile, version);
if (version != SECTION_FILE_VERSION) {
inputFile.close();
clearCache();
Serial.printf("Section state file: Unknown version %u\n", version);
return false;
}
int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft;
float fileLineCompression;
serialization::readPod(inputFile, fileFontId);
serialization::readPod(inputFile, fileLineCompression);
serialization::readPod(inputFile, fileMarginTop);
serialization::readPod(inputFile, fileMarginRight);
serialization::readPod(inputFile, fileMarginBottom);
serialization::readPod(inputFile, fileMarginLeft);
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
inputFile.close();
clearCache();
Serial.println("Section state file: Parameters do not match, ignoring");
return false;
}
}
serialization::readPod(inputFile, pageCount);
inputFile.close();
Serial.printf("Loaded cache: %d pages\n", pageCount);
@@ -63,7 +93,8 @@ void Section::setupCacheDir() const {
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); }
bool Section::persistPageDataToSD() {
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) {
const auto localPath = epub->getSpineItem(spineIndex);
// TODO: Should we get rid of this file all together?
@@ -83,8 +114,8 @@ bool Section::persistPageDataToSD() {
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
auto visitor =
EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, [this](const Page* page) { this->onPageComplete(page); });
auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); });
success = visitor.parseAndBuildPages();
SD.remove(tmpHtmlPath.c_str());
@@ -93,7 +124,7 @@ bool Section::persistPageDataToSD() {
return false;
}
writeCacheMetadata();
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft);
return true;
}

View File

@@ -2,29 +2,32 @@
#include "Epub.h"
class Page;
class EpdRenderer;
class GfxRenderer;
class Section {
Epub* epub;
const int spineIndex;
EpdRenderer& renderer;
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);
public:
int pageCount = 0;
int currentPage = 0;
explicit Section(Epub* epub, const int spineIndex, EpdRenderer& renderer)
explicit Section(Epub* epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
}
~Section() = default;
void writeCacheMetadata() const;
bool loadCacheMetadata();
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft);
void setupCacheDir() const;
void clearCache() const;
bool persistPageDataToSD();
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft);
Page* loadPageFromSD() const;
};

View File

@@ -1,6 +1,6 @@
#pragma once
class EpdRenderer;
class GfxRenderer;
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
class Block {
public:
virtual ~Block() = default;
virtual void layout(EpdRenderer& renderer) = 0;
virtual void layout(GfxRenderer& renderer) = 0;
virtual BlockType getType() = 0;
virtual bool isEmpty() = 0;
virtual void finish() {}

View File

@@ -1,6 +1,6 @@
#include "TextBlock.h"
#include <EpdRenderer.h>
#include <GfxRenderer.h>
#include <Serialization.h>
void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) {
@@ -10,10 +10,11 @@ void TextBlock::addWord(const std::string& word, const bool is_bold, const bool
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
}
std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
std::list<TextBlock*> TextBlock::splitIntoLines(const GfxRenderer& renderer, const int fontId,
const int horizontalMargin) {
const int totalWordCount = words.size();
const int pageWidth = renderer.getPageWidth();
const int spaceWidth = renderer.getSpaceWidth();
const int pageWidth = GfxRenderer::getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId);
words.shrink_to_fit();
wordStyles.shrink_to_fit();
@@ -21,7 +22,7 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
// measure each word
uint16_t wordWidths[totalWordCount];
for (int i = 0; i < words.size(); i++) {
for (int i = 0; i < totalWordCount; i++) {
// measure the word
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN) {
@@ -33,7 +34,7 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
const int width = renderer.getTextWidth(words[i].c_str(), fontStyle);
const int width = renderer.getTextWidth(fontId, words[i].c_str(), fontStyle);
wordWidths[i] = width;
}
@@ -154,7 +155,7 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
return lines;
}
void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) const {
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
for (int i = 0; i < words.size(); i++) {
// render the word
EpdFontStyle fontStyle = REGULAR;
@@ -165,7 +166,7 @@ void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) co
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer.drawText(x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
}
}

View File

@@ -40,10 +40,10 @@ class TextBlock final : public Block {
void setStyle(const BLOCK_STYLE style) { this->style = style; }
BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() override { return words.empty(); }
void layout(EpdRenderer& renderer) override {};
void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines
std::list<TextBlock*> splitIntoLines(const EpdRenderer& renderer);
void render(const EpdRenderer& renderer, int x, int y) const;
std::list<TextBlock*> 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);