From b743a1ca8e99a918f4918520e88eb2e882b360f5 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Mon, 8 Dec 2025 22:06:09 +1100 Subject: [PATCH] Remove EpdRenderer and create new GfxRenderer --- lib/EpdFontRenderer/EpdFontRenderer.hpp | 139 -------------- lib/EpdRenderer/EpdRenderer.cpp | 242 ------------------------ lib/EpdRenderer/EpdRenderer.h | 58 ------ lib/Epub/Epub/EpubHtmlParserSlim.cpp | 20 +- lib/Epub/Epub/EpubHtmlParserSlim.h | 34 +++- lib/Epub/Epub/Page.cpp | 24 ++- lib/Epub/Epub/Page.h | 17 +- lib/Epub/Epub/Section.cpp | 61 ++++-- lib/Epub/Epub/Section.h | 15 +- lib/Epub/Epub/blocks/Block.h | 4 +- lib/Epub/Epub/blocks/TextBlock.cpp | 17 +- lib/Epub/Epub/blocks/TextBlock.h | 6 +- lib/GfxRenderer/GfxRenderer.cpp | 222 ++++++++++++++++++++++ lib/GfxRenderer/GfxRenderer.h | 53 ++++++ src/config.h | 29 +++ src/main.cpp | 34 +++- src/screens/BootLogoScreen.cpp | 17 +- src/screens/BootLogoScreen.h | 2 +- src/screens/EpubReaderScreen.cpp | 83 ++++---- src/screens/EpubReaderScreen.h | 4 +- src/screens/FileSelectionScreen.cpp | 18 +- src/screens/FileSelectionScreen.h | 2 +- src/screens/FullScreenMessageScreen.cpp | 16 +- src/screens/FullScreenMessageScreen.h | 12 +- src/screens/Screen.h | 6 +- src/screens/SleepScreen.cpp | 17 +- src/screens/SleepScreen.h | 2 +- 27 files changed, 564 insertions(+), 590 deletions(-) delete mode 100644 lib/EpdFontRenderer/EpdFontRenderer.hpp delete mode 100644 lib/EpdRenderer/EpdRenderer.cpp delete mode 100644 lib/EpdRenderer/EpdRenderer.h create mode 100644 lib/GfxRenderer/GfxRenderer.cpp create mode 100644 lib/GfxRenderer/GfxRenderer.h create mode 100644 src/config.h diff --git a/lib/EpdFontRenderer/EpdFontRenderer.hpp b/lib/EpdFontRenderer/EpdFontRenderer.hpp deleted file mode 100644 index 4eff53f..0000000 --- a/lib/EpdFontRenderer/EpdFontRenderer.hpp +++ /dev/null @@ -1,139 +0,0 @@ -#pragma once -#include -#include -#include - -inline int min(const int a, const int b) { return a < b ? a : b; } -inline int max(const int a, const int b) { return a > b ? a : b; } - -enum EpdFontRendererMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; - -template -class EpdFontRenderer { - Renderable& renderer; - void renderChar(uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style = REGULAR, - EpdFontRendererMode mode = BW); - - public: - const EpdFontFamily* fontFamily; - explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer) - : fontFamily(fontFamily), renderer(renderer) {} - ~EpdFontRenderer() = default; - void renderString(const char* string, int* x, int* y, bool pixelState = true, EpdFontStyle style = REGULAR, - EpdFontRendererMode mode = BW); - void drawPixel(int x, int y, bool pixelState); -}; - -template -void EpdFontRenderer::renderString(const char* string, int* x, int* y, const bool pixelState, - const EpdFontStyle style, const EpdFontRendererMode mode) { - // cannot draw a NULL / empty string - if (string == nullptr || *string == '\0') { - return; - } - - // no printable characters - if (!fontFamily->hasPrintableChars(string, style)) { - return; - } - - uint32_t cp; - while ((cp = utf8NextCodepoint(reinterpret_cast(&string)))) { - renderChar(cp, x, y, pixelState, style, mode); - } - - *y += fontFamily->getData(style)->advanceY; -} - -// TODO: Consolidate this with EpdRenderer implementation -template -void EpdFontRenderer::drawPixel(const int x, const int y, const bool pixelState) { - uint8_t* frameBuffer = renderer.getFrameBuffer(); - - // Early return if no framebuffer is set - if (!frameBuffer) { - Serial.printf("!!No framebuffer\n"); - return; - } - - // Bounds checking (portrait: 480x800) - if (x < 0 || x >= EInkDisplay::DISPLAY_HEIGHT || y < 0 || y >= EInkDisplay::DISPLAY_WIDTH) { - Serial.printf("!!Outside range (%d, %d)\n", x, y); - return; - } - - // Rotate coordinates: portrait (480x800) -> landscape (800x480) - // Rotation: 90 degrees clockwise - const int16_t rotatedX = y; - const int16_t rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; - - // Calculate byte position and bit position - const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); - const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first - - // Set or clear the bit - if (pixelState) { - frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit - } else { - frameBuffer[byteIndex] |= (1 << bitPosition); // Set bit - } -} - -template -void EpdFontRenderer::renderChar(const uint32_t cp, int* x, const int* y, const bool pixelState, - const EpdFontStyle style, const EpdFontRendererMode mode) { - const EpdGlyph* glyph = fontFamily->getGlyph(cp, style); - if (!glyph) { - // TODO: Replace with fallback glyph property? - glyph = fontFamily->getGlyph('?', style); - } - - // no glyph? - if (!glyph) { - Serial.printf("No glyph for codepoint %d\n", cp); - return; - } - - const int is2Bit = fontFamily->getData(style)->is2Bit; - const uint32_t offset = glyph->dataOffset; - const uint8_t width = glyph->width; - const uint8_t height = glyph->height; - const int left = glyph->left; - - const uint8_t* bitmap = nullptr; - bitmap = &fontFamily->getData(style)->bitmap[offset]; - - if (bitmap != nullptr) { - for (int glyphY = 0; glyphY < height; glyphY++) { - int screenY = *y - glyph->top + glyphY; - for (int glyphX = 0; glyphX < width; glyphX++) { - const int pixelPosition = glyphY * width + glyphX; - int screenX = *x + left + glyphX; - - if (is2Bit) { - const uint8_t byte = bitmap[pixelPosition / 4]; - const uint8_t bit_index = (3 - pixelPosition % 4) * 2; - - const uint8_t val = (byte >> bit_index) & 0x3; - if (mode == BW && val > 0) { - drawPixel(screenX, screenY, pixelState); - } else if (mode == GRAYSCALE_MSB && val == 1) { - // TODO: Not sure how this anti-aliasing goes on black backgrounds - drawPixel(screenX, screenY, false); - } else if (mode == GRAYSCALE_LSB && val == 2) { - drawPixel(screenX, screenY, false); - } - } else { - const uint8_t byte = bitmap[pixelPosition / 8]; - const uint8_t bit_index = 7 - (pixelPosition % 8); - - if ((byte >> bit_index) & 1) { - drawPixel(screenX, screenY, pixelState); - } - } - } - } - } - - *x += glyph->advanceX; -} diff --git a/lib/EpdRenderer/EpdRenderer.cpp b/lib/EpdRenderer/EpdRenderer.cpp deleted file mode 100644 index f3eca77..0000000 --- a/lib/EpdRenderer/EpdRenderer.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include "EpdRenderer.h" - -#include "builtinFonts/babyblue.h" -#include "builtinFonts/bookerly_2b.h" -#include "builtinFonts/bookerly_bold_2b.h" -#include "builtinFonts/bookerly_bold_italic_2b.h" -#include "builtinFonts/bookerly_italic_2b.h" -#include "builtinFonts/ubuntu_10.h" -#include "builtinFonts/ubuntu_bold_10.h" - -EpdFont bookerlyFont(&bookerly_2b); -EpdFont bookerlyBoldFont(&bookerly_bold_2b); -EpdFont bookerlyItalicFont(&bookerly_italic_2b); -EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b); -EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont); - -EpdFont smallFont(&babyblue); -EpdFontFamily smallFontFamily(&smallFont); - -EpdFont ubuntu10Font(&ubuntu_10); -EpdFont ununtuBold10Font(&ubuntu_bold_10); -EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font); - -EpdRenderer::EpdRenderer(EInkDisplay& einkDisplay) - : einkDisplay(einkDisplay), - marginTop(11), - marginBottom(30), - marginLeft(10), - marginRight(10), - fontRendererMode(BW), - lineCompression(0.95f) { - this->regularFontRenderer = new EpdFontRenderer(&bookerlyFontFamily, einkDisplay); - this->smallFontRenderer = new EpdFontRenderer(&smallFontFamily, einkDisplay); - this->uiFontRenderer = new EpdFontRenderer(&ubuntuFontFamily, einkDisplay); -} - -EpdRenderer::~EpdRenderer() { - delete regularFontRenderer; - delete smallFontRenderer; - delete uiFontRenderer; -} - -void EpdRenderer::drawPixel(const int x, const int y, const bool state) const { - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); - - // Early return if no framebuffer is set - if (!frameBuffer) { - Serial.printf("!!No framebuffer\n"); - return; - } - - const int adjX = x + marginLeft; - const int adjY = y + marginTop; - - // Bounds checking (portrait: 480x800) - if (adjX < 0 || adjX >= EInkDisplay::DISPLAY_HEIGHT || adjY < 0 || adjY >= EInkDisplay::DISPLAY_WIDTH) { - Serial.printf("!!Outside range (%d, %d)\n", adjX, adjY); - return; - } - - // Rotate coordinates: portrait (480x800) -> landscape (800x480) - // Rotation: 90 degrees clockwise - const int rotatedX = adjY; - const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - adjX; - - // Calculate byte position and bit position - const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); - const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first - - if (state) { - frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit - } else { - frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit - } -} - -int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const { - int w = 0, h = 0; - - regularFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style); - - return w; -} - -int EpdRenderer::getUiTextWidth(const char* text, const EpdFontStyle style) const { - int w = 0, h = 0; - - uiFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style); - - return w; -} - -int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) const { - int w = 0, h = 0; - - smallFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style); - - return w; -} - -void EpdRenderer::drawText(const int x, const int y, const char* text, const bool state, - const EpdFontStyle style) const { - int ypos = y + getLineHeight() + marginTop; - int xpos = x + marginLeft; - regularFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode); -} - -void EpdRenderer::drawUiText(const int x, const int y, const char* text, const bool state, - const EpdFontStyle style) const { - int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop; - int xpos = x + marginLeft; - uiFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode); -} - -void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const bool state, - const EpdFontStyle style) const { - int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop; - int xpos = x + marginLeft; - smallFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode); -} - -void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height, - const EpdFontStyle style) const { - const size_t length = text.length(); - // fit the text into the box - int start = 0; - int end = 1; - int ypos = 0; - while (true) { - if (end >= length) { - drawText(x, y + ypos, text.substr(start, length - start).c_str(), true, style); - break; - } - - if (ypos + getLineHeight() >= height) { - break; - } - - if (text[end - 1] == '\n') { - drawText(x, y + ypos, text.substr(start, end - start).c_str(), true, style); - ypos += getLineHeight(); - start = end; - end = start + 1; - continue; - } - - if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) { - drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), true, style); - ypos += getLineHeight(); - start = end - 1; - continue; - } - - end++; - } -} - -void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { - if (x1 == x2) { - if (y2 < y1) { - std::swap(y1, y2); - } - for (int y = y1; y <= y2; y++) { - drawPixel(x1, y, state); - } - } else if (y1 == y2) { - if (x2 < x1) { - std::swap(x1, x2); - } - for (int x = x1; x <= x2; x++) { - drawPixel(x, y1, state); - } - } else { - // TODO: Implement - Serial.println("Line drawing not supported"); - } -} - -void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const { - drawLine(x, y, x + width - 1, y, state); - drawLine(x + width - 1, y, x + width - 1, y + height - 1, state); - drawLine(x + width - 1, y + height - 1, x, y + height - 1, state); - drawLine(x, y, x, y + height - 1, state); -} - -void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { - for (int fillY = y; fillY < y + height; fillY++) { - drawLine(x, fillY, x + width - 1, fillY, state); - } -} - -void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { - drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height); -} - -// TODO: Support y-mirror? -void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width, - const int height) const { - einkDisplay.drawImage(bitmap, x, y, width, height); -} - -void EpdRenderer::clearScreen(const uint8_t color) const { - Serial.println("Clearing screen"); - einkDisplay.clearScreen(color); -} - -void EpdRenderer::invertScreen() const { - uint8_t *buffer = einkDisplay.getFrameBuffer(); - for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { - buffer[i] = ~buffer[i]; - } -} - -void EpdRenderer::flushDisplay(const EInkDisplay::RefreshMode refreshMode) const { - einkDisplay.displayBuffer(refreshMode); -} - -// TODO: Support partial window update -// void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const { -// const int rotatedX = y; -// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; -// -// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width); -// } - -int EpdRenderer::getPageWidth() const { return EInkDisplay::DISPLAY_HEIGHT - marginLeft - marginRight; } - -int EpdRenderer::getPageHeight() const { return EInkDisplay::DISPLAY_WIDTH - marginTop - marginBottom; } - -int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; } - -int EpdRenderer::getLineHeight() const { - return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression; -} - -void EpdRenderer::swapBuffers() const { einkDisplay.swapBuffers(); } - -void EpdRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } - -void EpdRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); } - -void EpdRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } diff --git a/lib/EpdRenderer/EpdRenderer.h b/lib/EpdRenderer/EpdRenderer.h deleted file mode 100644 index a9e76d3..0000000 --- a/lib/EpdRenderer/EpdRenderer.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include - -#include - -class EpdRenderer { - EInkDisplay& einkDisplay; - EpdFontRenderer* regularFontRenderer; - EpdFontRenderer* smallFontRenderer; - EpdFontRenderer* uiFontRenderer; - int marginTop; - int marginBottom; - int marginLeft; - int marginRight; - EpdFontRendererMode fontRendererMode; - float lineCompression; - - public: - explicit EpdRenderer(EInkDisplay& einkDisplay); - ~EpdRenderer(); - void drawPixel(int x, int y, bool state = true) const; - int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const; - int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const; - int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const; - void drawText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const; - void drawUiText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const; - void drawSmallText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const; - void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const; - void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; - void drawRect(int x, int y, int width, int height, bool state = true) const; - void fillRect(int x, int y, int width, int height, bool state = true) const; - void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; - void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height) const; - - void swapBuffers() const; - void copyGrayscaleLsbBuffers() const; - void copyGrayscaleMsbBuffers() const; - void displayGrayBuffer() const; - void clearScreen(uint8_t color = 0xFF) const; - - void invertScreen() const; - - void flushDisplay(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; - // void flushArea(int x, int y, int width, int height) const; - - int getPageWidth() const; - int getPageHeight() const; - int getSpaceWidth() const; - int getLineHeight() const; - - // set margins - void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; } - void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; } - void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; } - void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; } - void setFontRendererMode(const EpdFontRendererMode mode) { this->fontRendererMode = mode; } -}; diff --git a/lib/Epub/Epub/EpubHtmlParserSlim.cpp b/lib/Epub/Epub/EpubHtmlParserSlim.cpp index 50e588d..b451e8e 100644 --- a/lib/Epub/Epub/EpubHtmlParserSlim.cpp +++ b/lib/Epub/Epub/EpubHtmlParserSlim.cpp @@ -1,6 +1,6 @@ #include "EpubHtmlParserSlim.h" -#include +#include #include #include @@ -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) { diff --git a/lib/Epub/Epub/EpubHtmlParserSlim.h b/lib/Epub/Epub/EpubHtmlParserSlim.h index ae42f3e..dafff79 100644 --- a/lib/Epub/Epub/EpubHtmlParserSlim.h +++ b/lib/Epub/Epub/EpubHtmlParserSlim.h @@ -1,30 +1,38 @@ #pragma once #include -#include +#include #include #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 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& 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(); }; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 49fe4f8..2e82838 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -3,9 +3,12 @@ #include #include -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); diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 5671090..9d014af 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -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 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 elements; + void render(GfxRenderer& renderer, int fontId) const; void serialize(std::ostream& os) const; static Page* deserialize(std::istream& is); }; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 23e49f1..27b4b9a 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -1,6 +1,6 @@ #include "Section.h" -#include +#include #include #include @@ -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; } diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 6c0e5ad..036a42d 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -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; }; diff --git a/lib/Epub/Epub/blocks/Block.h b/lib/Epub/Epub/blocks/Block.h index 3f58e54..d83e771 100644 --- a/lib/Epub/Epub/blocks/Block.h +++ b/lib/Epub/Epub/blocks/Block.h @@ -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() {} diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 191cc48..2b3bd49 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -1,6 +1,6 @@ #include "TextBlock.h" -#include +#include #include 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::splitIntoLines(const EpdRenderer& renderer) { +std::list 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::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::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::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); } } diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 90ef919..afd3178 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -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 splitIntoLines(const EpdRenderer& renderer); - void render(const EpdRenderer& renderer, int x, int y) const; + 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); diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp new file mode 100644 index 0000000..6f8cfc2 --- /dev/null +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -0,0 +1,222 @@ +#include "GfxRenderer.h" + +#include + +void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } + +void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { + uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + + // Early return if no framebuffer is set + if (!frameBuffer) { + Serial.printf("!!No framebuffer\n"); + return; + } + + // Rotate coordinates: portrait (480x800) -> landscape (800x480) + // Rotation: 90 degrees clockwise + const int rotatedX = y; + const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; + + // Bounds checking (portrait: 480x800) + if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 || + rotatedY >= EInkDisplay::DISPLAY_HEIGHT) { + Serial.printf("!! Outside range (%d, %d)\n", x, y); + return; + } + + // Calculate byte position and bit position + const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); + const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first + + if (state) { + frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit + } else { + frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit + } +} + +int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontStyle style) const { + if (fontMap.count(fontId) == 0) { + Serial.printf("Font %d not found\n", fontId); + return 0; + } + + int w = 0, h = 0; + fontMap.at(fontId).getTextDimensions(text, &w, &h, style); + return w; +} + +void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, + const EpdFontStyle style) const { + const int yPos = y + getLineHeight(fontId); + int xpos = x; + + // cannot draw a NULL / empty string + if (text == nullptr || *text == '\0') { + return; + } + + if (fontMap.count(fontId) == 0) { + Serial.printf("Font %d not found\n", fontId); + return; + } + const auto font = fontMap.at(fontId); + + // no printable characters + if (!font.hasPrintableChars(text, style)) { + return; + } + + uint32_t cp; + while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + renderChar(font, cp, &xpos, &yPos, black, style); + } +} + +void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { + if (x1 == x2) { + if (y2 < y1) { + std::swap(y1, y2); + } + for (int y = y1; y <= y2; y++) { + drawPixel(x1, y, state); + } + } else if (y1 == y2) { + if (x2 < x1) { + std::swap(x1, x2); + } + for (int x = x1; x <= x2; x++) { + drawPixel(x, y1, state); + } + } else { + // TODO: Implement + Serial.println("Line drawing not supported"); + } +} + +void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const { + drawLine(x, y, x + width - 1, y, state); + drawLine(x + width - 1, y, x + width - 1, y + height - 1, state); + drawLine(x + width - 1, y + height - 1, x, y + height - 1, state); + drawLine(x, y, x, y + height - 1, state); +} + +void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { + for (int fillY = y; fillY < y + height; fillY++) { + drawLine(x, fillY, x + width - 1, fillY, state); + } +} + +void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { + einkDisplay.drawImage(bitmap, x, y, width, height); +} + +void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); } + +void GfxRenderer::invertScreen() const { + uint8_t* buffer = einkDisplay.getFrameBuffer(); + for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { + buffer[i] = ~buffer[i]; + } +} + +void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const { + einkDisplay.displayBuffer(refreshMode); +} + +// TODO: Support partial window update +// void GfxRenderer::flushArea(const int x, const int y, const int width, const int height) const { +// const int rotatedX = y; +// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; +// +// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width); +// } + +// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation +int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; } +int GfxRenderer::getScreenHeight() { return EInkDisplay::DISPLAY_WIDTH; } + +int GfxRenderer::getSpaceWidth(const int fontId) const { + if (fontMap.count(fontId) == 0) { + Serial.printf("Font %d not found\n", fontId); + return 0; + } + + return fontMap.at(fontId).getGlyph(' ', REGULAR)->advanceX; +} + +int GfxRenderer::getLineHeight(const int fontId) const { + if (fontMap.count(fontId) == 0) { + Serial.printf("Font %d not found\n", fontId); + return 0; + } + + return fontMap.at(fontId).getData(REGULAR)->advanceY; +} + +void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); } + +void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } + +void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); } + +void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } + +void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y, + const bool pixelState, const EpdFontStyle style) const { + const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); + if (!glyph) { + // TODO: Replace with fallback glyph property? + glyph = fontFamily.getGlyph('?', style); + } + + // no glyph? + if (!glyph) { + Serial.printf("No glyph for codepoint %d\n", cp); + return; + } + + const int is2Bit = fontFamily.getData(style)->is2Bit; + const uint32_t offset = glyph->dataOffset; + const uint8_t width = glyph->width; + const uint8_t height = glyph->height; + const int left = glyph->left; + + const uint8_t* bitmap = nullptr; + bitmap = &fontFamily.getData(style)->bitmap[offset]; + + if (bitmap != nullptr) { + for (int glyphY = 0; glyphY < height; glyphY++) { + const int screenY = *y - glyph->top + glyphY; + for (int glyphX = 0; glyphX < width; glyphX++) { + const int pixelPosition = glyphY * width + glyphX; + const int screenX = *x + left + glyphX; + + if (is2Bit) { + const uint8_t byte = bitmap[pixelPosition / 4]; + const uint8_t bit_index = (3 - pixelPosition % 4) * 2; + + const uint8_t val = (byte >> bit_index) & 0x3; + if (fontRenderMode == BW && val > 0) { + drawPixel(screenX, screenY, pixelState); + } else if (fontRenderMode == GRAYSCALE_MSB && val == 1) { + // TODO: Not sure how this anti-aliasing goes on black backgrounds + drawPixel(screenX, screenY, false); + } else if (fontRenderMode == GRAYSCALE_LSB && val == 2) { + drawPixel(screenX, screenY, false); + } + } else { + const uint8_t byte = bitmap[pixelPosition / 8]; + const uint8_t bit_index = 7 - (pixelPosition % 8); + + if ((byte >> bit_index) & 1) { + drawPixel(screenX, screenY, pixelState); + } + } + } + } + } + + *x += glyph->advanceX; +} diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h new file mode 100644 index 0000000..96c8e11 --- /dev/null +++ b/lib/GfxRenderer/GfxRenderer.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include + +#include "EpdFontFamily.h" + +class GfxRenderer { + public: + enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; + + private: + EInkDisplay& einkDisplay; + FontRenderMode fontRenderMode; + std::map fontMap; + void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, + EpdFontStyle style) const; + + public: + explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), fontRenderMode(BW) {} + ~GfxRenderer() = default; + + // Setup + void insertFont(int fontId, EpdFontFamily font); + + // Screen ops + static int getScreenWidth(); + static int getScreenHeight(); + void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; + void invertScreen() const; + void clearScreen(uint8_t color = 0xFF) const; + + // Drawing + void drawPixel(int x, int y, bool state = true) const; + void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; + void drawRect(int x, int y, int width, int height, bool state = true) const; + void fillRect(int x, int y, int width, int height, bool state = true) const; + void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; + + // Text + int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const; + void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const; + void setFontRenderMode(const FontRenderMode mode) { this->fontRenderMode = mode; } + int getSpaceWidth(int fontId) const; + int getLineHeight(int fontId) const; + + // Low level functions + void swapBuffers() const; + void copyGrayscaleLsbBuffers() const; + void copyGrayscaleMsbBuffers() const; + void displayGrayBuffer() const; +}; diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..613a02b --- /dev/null +++ b/src/config.h @@ -0,0 +1,29 @@ +#pragma once + +/** + * Generated with: + * ruby -rdigest -e 'puts [ + * "./lib/EpdFont/builtinFonts/bookerly_2b.h", + * "./lib/EpdFont/builtinFonts/bookerly_bold_2b.h", + * "./lib/EpdFont/builtinFonts/bookerly_bold_italic_2b.h", + * "./lib/EpdFont/builtinFonts/bookerly_italic_2b.h", + * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' + */ +#define READER_FONT_ID 1747632454 + +/** + * Generated with: + * ruby -rdigest -e 'puts [ + * "./lib/EpdFont/builtinFonts/ubuntu_10.h", + * "./lib/EpdFont/builtinFonts/ubuntu_bold_10.h", + * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' + */ +#define UI_FONT_ID 225955604 + +/** + * Generated with: + * ruby -rdigest -e 'puts [ + * "./lib/EpdFont/builtinFonts/babyblue.h", + * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' + */ +#define SMALL_FONT_ID 141891058 diff --git a/src/main.cpp b/src/main.cpp index 22e6053..640070a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,21 @@ #include #include -#include #include +#include #include #include #include #include "Battery.h" #include "CrossPointState.h" +#include "builtinFonts/babyblue.h" +#include "builtinFonts/bookerly_2b.h" +#include "builtinFonts/bookerly_bold_2b.h" +#include "builtinFonts/bookerly_bold_italic_2b.h" +#include "builtinFonts/bookerly_italic_2b.h" +#include "builtinFonts/ubuntu_10.h" +#include "builtinFonts/ubuntu_bold_10.h" +#include "config.h" #include "screens/BootLogoScreen.h" #include "screens/EpubReaderScreen.h" #include "screens/FileSelectionScreen.h" @@ -30,10 +38,24 @@ EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); InputManager inputManager; -EpdRenderer renderer(einkDisplay); +GfxRenderer renderer(einkDisplay); Screen* currentScreen; CrossPointState appState; +// Fonts +EpdFont bookerlyFont(&bookerly_2b); +EpdFont bookerlyBoldFont(&bookerly_bold_2b); +EpdFont bookerlyItalicFont(&bookerly_italic_2b); +EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b); +EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont); + +EpdFont smallFont(&babyblue); +EpdFontFamily smallFontFamily(&smallFont); + +EpdFont ubuntu10Font(&ubuntu_10); +EpdFont ubuntuBold10Font(&ubuntu_bold_10); +EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font); + // Power button timing // Time required to confirm boot from sleep constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000; @@ -141,7 +163,8 @@ void onSelectEpubFile(const std::string& path) { enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome)); } else { exitScreen(); - enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH)); + enterNewScreen( + new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH)); delay(2000); onGoHome(); } @@ -172,6 +195,11 @@ void setup() { einkDisplay.begin(); Serial.println("Display initialized"); + renderer.insertFont(READER_FONT_ID, bookerlyFontFamily); + renderer.insertFont(UI_FONT_ID, ubuntuFontFamily); + renderer.insertFont(SMALL_FONT_ID, smallFontFamily); + Serial.println("Fonts loaded"); + exitScreen(); enterNewScreen(new BootLogoScreen(renderer, inputManager)); diff --git a/src/screens/BootLogoScreen.cpp b/src/screens/BootLogoScreen.cpp index 2113a07..5688c7e 100644 --- a/src/screens/BootLogoScreen.cpp +++ b/src/screens/BootLogoScreen.cpp @@ -1,19 +1,20 @@ #include "BootLogoScreen.h" -#include +#include +#include "config.h" #include "images/CrossLarge.h" void BootLogoScreen::onEnter() { - const auto pageWidth = renderer.getPageWidth(); - const auto pageHeight = renderer.getPageHeight(); + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); renderer.clearScreen(); // Location for images is from top right in landscape orientation renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128); - const int width = renderer.getUiTextWidth("CrossPoint", BOLD); - renderer.drawUiText((pageWidth - width)/ 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD); - const int bootingWidth = renderer.getSmallTextWidth("BOOTING"); - renderer.drawSmallText((pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "BOOTING"); - renderer.flushDisplay(); + const int width = renderer.getTextWidth(UI_FONT_ID, "CrossPoint", BOLD); + renderer.drawText(UI_FONT_ID, (pageWidth - width) / 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD); + const int bootingWidth = renderer.getTextWidth(SMALL_FONT_ID, "BOOTING"); + renderer.drawText(SMALL_FONT_ID, (pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "BOOTING"); + renderer.displayBuffer(); } diff --git a/src/screens/BootLogoScreen.h b/src/screens/BootLogoScreen.h index 58d99f0..503afac 100644 --- a/src/screens/BootLogoScreen.h +++ b/src/screens/BootLogoScreen.h @@ -3,6 +3,6 @@ class BootLogoScreen final : public Screen { public: - explicit BootLogoScreen(EpdRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {} + explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {} void onEnter() override; }; diff --git a/src/screens/EpubReaderScreen.cpp b/src/screens/EpubReaderScreen.cpp index 3201a0d..c521de4 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/screens/EpubReaderScreen.cpp @@ -1,13 +1,19 @@ #include "EpubReaderScreen.h" -#include #include +#include #include #include "Battery.h" +#include "config.h" constexpr int PAGES_PER_REFRESH = 15; constexpr unsigned long SKIP_CHAPTER_MS = 700; +constexpr float lineCompression = 0.95f; +constexpr int marginTop = 11; +constexpr int marginRight = 10; +constexpr int marginBottom = 30; +constexpr int marginLeft = 10; void EpubReaderScreen::taskTrampoline(void* param) { auto* self = static_cast(param); @@ -150,26 +156,28 @@ void EpubReaderScreen::renderScreen() { const auto filepath = epub->getSpineItem(currentSpineIndex); Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex); section = new Section(epub, currentSpineIndex, renderer); - if (!section->loadCacheMetadata()) { + if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, + marginLeft)) { Serial.println("Cache not found, building..."); { - const int textWidth = renderer.getTextWidth("Indexing..."); + const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing..."); constexpr int margin = 20; - const int x = (renderer.getPageWidth() - textWidth - margin * 2) / 2; + const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2; constexpr int y = 50; const int w = textWidth + margin * 2; - const int h = renderer.getLineHeight() + margin * 2; + const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2; renderer.swapBuffers(); renderer.fillRect(x, y, w, h, 0); - renderer.drawText(x + margin, y + margin, "Indexing..."); + renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing..."); renderer.drawRect(x + 5, y + 5, w - 10, h - 10); - renderer.flushDisplay(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); pagesUntilFullRefresh = 0; } section->setupCacheDir(); - if (!section->persistPageDataToSD()) { + if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, + marginLeft)) { Serial.println("Failed to persist page data to SD"); delete section; section = nullptr; @@ -190,19 +198,19 @@ void EpubReaderScreen::renderScreen() { if (section->pageCount == 0) { Serial.println("No pages to render"); - const int width = renderer.getTextWidth("Empty chapter", BOLD); - renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Empty chapter", true, BOLD); + const int width = renderer.getTextWidth(READER_FONT_ID, "Empty chapter", BOLD); + renderer.drawText(READER_FONT_ID, (GfxRenderer::getScreenWidth() - width) / 2, 300, "Empty chapter", true, BOLD); renderStatusBar(); - renderer.flushDisplay(); + renderer.displayBuffer(); return; } if (section->currentPage < 0 || section->currentPage >= section->pageCount) { Serial.printf("Page out of bounds: %d (max %d)\n", section->currentPage, section->pageCount); - const int width = renderer.getTextWidth("Out of bounds", BOLD); - renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Out of bounds", true, BOLD); + const int width = renderer.getTextWidth(READER_FONT_ID, "Out of bounds", BOLD); + renderer.drawText(READER_FONT_ID, (GfxRenderer::getScreenWidth() - width) / 2, 300, "Out of bounds", true, BOLD); renderStatusBar(); - renderer.flushDisplay(); + renderer.displayBuffer(); return; } @@ -221,52 +229,54 @@ void EpubReaderScreen::renderScreen() { } void EpubReaderScreen::renderContents(const Page* p) { - p->render(renderer); + p->render(renderer, READER_FONT_ID); renderStatusBar(); if (pagesUntilFullRefresh <= 1) { - renderer.flushDisplay(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); pagesUntilFullRefresh = PAGES_PER_REFRESH; } else { - renderer.flushDisplay(); + renderer.displayBuffer(); pagesUntilFullRefresh--; } // grayscale rendering + // TODO: Only do this if font supports it { renderer.clearScreen(0x00); - renderer.setFontRendererMode(GRAYSCALE_LSB); - p->render(renderer); + renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB); + p->render(renderer, READER_FONT_ID); renderer.copyGrayscaleLsbBuffers(); // Render and copy to MSB buffer renderer.clearScreen(0x00); - renderer.setFontRendererMode(GRAYSCALE_MSB); - p->render(renderer); + renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB); + p->render(renderer, READER_FONT_ID); renderer.copyGrayscaleMsbBuffers(); // display grayscale part renderer.displayGrayBuffer(); - renderer.setFontRendererMode(BW); + renderer.setFontRenderMode(GfxRenderer::BW); } } void EpubReaderScreen::renderStatusBar() const { - const auto pageWidth = renderer.getPageWidth(); - + // Right aligned text for progress counter const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount); - const auto progressTextWidth = renderer.getSmallTextWidth(progress.c_str()); - renderer.drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str()); + const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); + renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, 776, + progress.c_str()); + // Left aligned battery icon and percentage const uint16_t percentage = battery.readPercentage(); const auto percentageText = std::to_string(percentage) + "%"; - const auto percentageTextWidth = renderer.getSmallTextWidth(percentageText.c_str()); - renderer.drawSmallText(20, 765, percentageText.c_str()); + const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, 776, percentageText.c_str()); // 1 column on left, 2 columns on right, 5 columns of battery body constexpr int batteryWidth = 15; constexpr int batteryHeight = 10; - constexpr int x = 0; - constexpr int y = 772; + constexpr int x = marginLeft; + constexpr int y = 783; // Top line renderer.drawLine(x, y, x + batteryWidth - 4, y); @@ -287,17 +297,18 @@ void EpubReaderScreen::renderStatusBar() const { } renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2); + // Centered chatper title text // Page width minus existing content with 30px padding on each side - const int leftMargin = 20 + percentageTextWidth + 30; - const int rightMargin = progressTextWidth + 30; - const int availableTextWidth = pageWidth - leftMargin - rightMargin; + const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft; + const int titleMarginRight = progressTextWidth + 30 + marginRight; + const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight; const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex)); auto title = tocItem.title; - int titleWidth = renderer.getSmallTextWidth(title.c_str()); + int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); while (titleWidth > availableTextWidth) { title = title.substr(0, title.length() - 8) + "..."; - titleWidth = renderer.getSmallTextWidth(title.c_str()); + titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); } - renderer.drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str()); + renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, 777, title.c_str()); } diff --git a/src/screens/EpubReaderScreen.h b/src/screens/EpubReaderScreen.h index 81ffdcf..2301769 100644 --- a/src/screens/EpubReaderScreen.h +++ b/src/screens/EpubReaderScreen.h @@ -21,11 +21,11 @@ class EpubReaderScreen final : public Screen { static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void renderScreen(); - void renderContents(const Page *p); + void renderContents(const Page* p); void renderStatusBar() const; public: - explicit EpubReaderScreen(EpdRenderer& renderer, InputManager& inputManager, Epub* epub, + explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, Epub* epub, const std::function& onGoHome) : Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {} void onEnter() override; diff --git a/src/screens/FileSelectionScreen.cpp b/src/screens/FileSelectionScreen.cpp index 557631f..6633978 100644 --- a/src/screens/FileSelectionScreen.cpp +++ b/src/screens/FileSelectionScreen.cpp @@ -1,8 +1,10 @@ #include "FileSelectionScreen.h" -#include +#include #include +#include "config.h" + void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { if (str1.back() == '/' && str2.back() != '/') return true; @@ -118,21 +120,21 @@ void FileSelectionScreen::displayTaskLoop() { void FileSelectionScreen::render() const { renderer.clearScreen(); - const auto pageWidth = renderer.getPageWidth(); - const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD); - renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", true, BOLD); + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto titleWidth = renderer.getTextWidth(READER_FONT_ID, "CrossPoint Reader", BOLD); + renderer.drawText(READER_FONT_ID, (pageWidth - titleWidth) / 2, 10, "CrossPoint Reader", true, BOLD); if (files.empty()) { - renderer.drawUiText(10, 50, "No EPUBs found"); + renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); } else { // Draw selection - renderer.fillRect(0, 50 + selectorIndex * 30 + 2, pageWidth - 1, 30); + renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30); for (size_t i = 0; i < files.size(); i++) { const auto file = files[i]; - renderer.drawUiText(10, 50 + i * 30, file.c_str(), i != selectorIndex); + renderer.drawText(UI_FONT_ID, 20, 60 + i * 30, file.c_str(), i != selectorIndex); } } - renderer.flushDisplay(); + renderer.displayBuffer(); } diff --git a/src/screens/FileSelectionScreen.h b/src/screens/FileSelectionScreen.h index 66f3b01..f0edc3b 100644 --- a/src/screens/FileSelectionScreen.h +++ b/src/screens/FileSelectionScreen.h @@ -24,7 +24,7 @@ class FileSelectionScreen final : public Screen { void loadFiles(); public: - explicit FileSelectionScreen(EpdRenderer& renderer, InputManager& inputManager, + explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onSelect) : Screen(renderer, inputManager), onSelect(onSelect) {} void onEnter() override; diff --git a/src/screens/FullScreenMessageScreen.cpp b/src/screens/FullScreenMessageScreen.cpp index 7110dd0..9ad96ea 100644 --- a/src/screens/FullScreenMessageScreen.cpp +++ b/src/screens/FullScreenMessageScreen.cpp @@ -1,14 +1,16 @@ #include "FullScreenMessageScreen.h" -#include +#include + +#include "config.h" void FullScreenMessageScreen::onEnter() { - const auto width = renderer.getUiTextWidth(text.c_str(), style); - const auto height = renderer.getLineHeight(); - const auto left = (renderer.getPageWidth() - width) / 2; - const auto top = (renderer.getPageHeight() - height) / 2; + const auto width = renderer.getTextWidth(UI_FONT_ID, text.c_str(), style); + const auto height = renderer.getLineHeight(UI_FONT_ID); + const auto left = (GfxRenderer::getScreenWidth() - width) / 2; + const auto top = (GfxRenderer::getScreenHeight() - height) / 2; renderer.clearScreen(); - renderer.drawUiText(left, top, text.c_str(), true, style); - renderer.flushDisplay(refreshMode); + renderer.drawText(UI_FONT_ID, left, top, text.c_str(), true, style); + renderer.displayBuffer(refreshMode); } diff --git a/src/screens/FullScreenMessageScreen.h b/src/screens/FullScreenMessageScreen.h index cdc6f01..e90abb6 100644 --- a/src/screens/FullScreenMessageScreen.h +++ b/src/screens/FullScreenMessageScreen.h @@ -1,9 +1,10 @@ #pragma once +#include +#include + #include #include -#include -#include #include "Screen.h" class FullScreenMessageScreen final : public Screen { @@ -12,12 +13,9 @@ class FullScreenMessageScreen final : public Screen { EInkDisplay::RefreshMode refreshMode; public: - explicit FullScreenMessageScreen(EpdRenderer& renderer, InputManager& inputManager, std::string text, + explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text, const EpdFontStyle style = REGULAR, const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) - : Screen(renderer, inputManager), - text(std::move(text)), - style(style), - refreshMode(refreshMode) {} + : Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {} void onEnter() override; }; diff --git a/src/screens/Screen.h b/src/screens/Screen.h index 6888c55..7e4b866 100644 --- a/src/screens/Screen.h +++ b/src/screens/Screen.h @@ -1,15 +1,15 @@ #pragma once #include -class EpdRenderer; +class GfxRenderer; class Screen { protected: - EpdRenderer& renderer; + GfxRenderer& renderer; InputManager& inputManager; public: - explicit Screen(EpdRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {} + explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {} virtual ~Screen() = default; virtual void onEnter() {} virtual void onExit() {} diff --git a/src/screens/SleepScreen.cpp b/src/screens/SleepScreen.cpp index 0099aa9..d384bef 100644 --- a/src/screens/SleepScreen.cpp +++ b/src/screens/SleepScreen.cpp @@ -1,19 +1,20 @@ #include "SleepScreen.h" -#include +#include +#include "config.h" #include "images/CrossLarge.h" void SleepScreen::onEnter() { - const auto pageWidth = renderer.getPageWidth(); - const auto pageHeight = renderer.getPageHeight(); + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); renderer.clearScreen(); renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128); - const int width = renderer.getUiTextWidth("CrossPoint", BOLD); - renderer.drawUiText((pageWidth - width)/ 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD); - const int bootingWidth = renderer.getSmallTextWidth("SLEEPING"); - renderer.drawSmallText((pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "SLEEPING"); + const int width = renderer.getTextWidth(UI_FONT_ID, "CrossPoint", BOLD); + renderer.drawText(UI_FONT_ID, (pageWidth - width) / 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD); + const int bootingWidth = renderer.getTextWidth(SMALL_FONT_ID, "SLEEPING"); + renderer.drawText(SMALL_FONT_ID, (pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "SLEEPING"); renderer.invertScreen(); - renderer.flushDisplay(EInkDisplay::FULL_REFRESH); + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); } diff --git a/src/screens/SleepScreen.h b/src/screens/SleepScreen.h index c56bada..b280087 100644 --- a/src/screens/SleepScreen.h +++ b/src/screens/SleepScreen.h @@ -3,6 +3,6 @@ class SleepScreen final : public Screen { public: - explicit SleepScreen(EpdRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {} + explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {} void onEnter() override; };