From aa2467de9e4ee937efd8f1345dd473c280317085 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sun, 1 Feb 2026 22:05:39 -0800 Subject: [PATCH] feat: Add inverse display setting with proper anti-aliasing Adds an inverseDisplay setting that renders white text on a black background in both EPUB and TXT readers. The grayscale anti-aliasing passes swap the LSB gray level condition based on text direction (pixelState/black parameter) so intermediate gray values render correctly against the dark background. --- lib/Epub/Epub/Page.cpp | 9 +++++---- lib/Epub/Epub/Page.h | 6 +++--- lib/Epub/Epub/blocks/TextBlock.cpp | 5 +++-- lib/Epub/Epub/blocks/TextBlock.h | 2 +- lib/GfxRenderer/GfxRenderer.cpp | 6 +++--- src/CrossPointSettings.cpp | 5 ++++- src/CrossPointSettings.h | 2 ++ src/activities/reader/EpubReaderActivity.cpp | 14 ++++++++------ src/activities/reader/TxtReaderActivity.cpp | 6 ++++-- src/activities/settings/SettingsActivity.cpp | 5 +++-- 10 files changed, 36 insertions(+), 24 deletions(-) diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 92839eb7..aa09d2c6 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -3,8 +3,8 @@ #include #include -void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { - block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); +void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, const bool black) { + block->render(renderer, fontId, xPos + xOffset, yPos + yOffset, black); } bool PageLine::serialize(FsFile& file) { @@ -25,9 +25,10 @@ std::unique_ptr PageLine::deserialize(FsFile& file) { return std::unique_ptr(new PageLine(std::move(tb), xPos, yPos)); } -void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { +void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, + const bool black) const { for (auto& element : elements) { - element->render(renderer, fontId, xOffset, yOffset); + element->render(renderer, fontId, xOffset, yOffset, black); } } diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 20061941..9662b2d7 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -17,7 +17,7 @@ class PageElement { int16_t yPos; explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} virtual ~PageElement() = default; - virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0; + virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) = 0; virtual bool serialize(FsFile& file) = 0; }; @@ -28,7 +28,7 @@ class PageLine final : public PageElement { public: PageLine(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), block(std::move(block)) {} - void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) override; bool serialize(FsFile& file) override; static std::unique_ptr deserialize(FsFile& file); }; @@ -37,7 +37,7 @@ class Page { public: // the list of block index and line numbers on this page std::vector> elements; - void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); }; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 2a15aef0..7d4a300f 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -3,7 +3,8 @@ #include #include -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 bool black) const { // Validate iterator bounds before rendering if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), @@ -16,7 +17,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int auto wordXposIt = wordXpos.begin(); for (size_t i = 0; i < words.size(); i++) { - renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt); + renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), black, *wordStylesIt); std::advance(wordIt, 1); std::advance(wordStylesIt, 1); diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 415a18f3..e0185688 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -34,7 +34,7 @@ class TextBlock final : public Block { bool isEmpty() override { return words.empty(); } void layout(GfxRenderer& renderer) override {}; // given a renderer works out where to break the words into lines - void render(const GfxRenderer& renderer, int fontId, int x, int y) const; + void render(const GfxRenderer& renderer, int fontId, int x, int y, bool black = true) const; BlockType getType() override { return TEXT_BLOCK; } bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index b5aa7710..387002f1 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -638,7 +638,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y drawPixel(screenX, screenY, black); } else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { + } else if (renderMode == GRAYSCALE_LSB && bmpVal == (black ? 1 : 2)) { drawPixel(screenX, screenY, false); } } else { @@ -822,8 +822,8 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, // Light gray (also mark the MSB if it's going to be a dark gray too) // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { - // Dark gray + } else if (renderMode == GRAYSCALE_LSB && bmpVal == (pixelState ? 1 : 2)) { + // Dark gray (swap gray level for inverse display) drawPixel(screenX, screenY, false); } } else { diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 232c7c57..badcd143 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 23; +constexpr uint8_t SETTINGS_COUNT = 24; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -60,6 +60,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsPassword)); serialization::writePod(outputFile, sleepScreenCoverFilter); + serialization::writePod(outputFile, inverseDisplay); // New fields added at end for backward compatibility outputFile.close(); @@ -148,6 +149,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, inverseDisplay); + if (++settingsRead >= fileSettingsCount) break; // New fields added at end for backward compatibility } while (false); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c450d348..4674d2df 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -137,6 +137,8 @@ class CrossPointSettings { uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press chapter skip on side buttons uint8_t longPressChapterSkip = 1; + // Inverse display (white text on black background) + uint8_t inverseDisplay = 0; ~CrossPointSettings() = default; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5ccfb4fe..10ca42be 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -400,11 +400,12 @@ void EpubReaderActivity::renderScreen() { } } - renderer.clearScreen(); + const bool inverse = SETTINGS.inverseDisplay; + renderer.clearScreen(inverse ? 0x00 : 0xFF); if (section->pageCount == 0) { Serial.printf("[%lu] [ERS] No pages to render\n", millis()); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", !inverse, EpdFontFamily::BOLD); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; @@ -412,7 +413,7 @@ void EpubReaderActivity::renderScreen() { if (section->currentPage < 0 || section->currentPage >= section->pageCount) { Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", !inverse, EpdFontFamily::BOLD); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; @@ -453,7 +454,8 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + const bool inverse = SETTINGS.inverseDisplay; + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(HalDisplay::HALF_REFRESH); @@ -471,13 +473,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or if (SETTINGS.textAntiAliasing) { renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse); renderer.copyGrayscaleLsbBuffers(); // Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse); renderer.copyGrayscaleMsbBuffers(); // display grayscale part diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index eb1a9eef..24bcebf9 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -393,7 +393,7 @@ void TxtReaderActivity::renderScreen() { currentPageLines.clear(); loadPageAtOffset(offset, currentPageLines, nextOffset); - renderer.clearScreen(); + renderer.clearScreen(SETTINGS.inverseDisplay ? 0x00 : 0xFF); renderPage(); // Save progress @@ -412,6 +412,8 @@ void TxtReaderActivity::renderPage() { const int lineHeight = renderer.getLineHeight(cachedFontId); const int contentWidth = viewportWidth; + const bool inverse = SETTINGS.inverseDisplay; + // Render text lines with alignment auto renderLines = [&]() { int y = orientedMarginTop; @@ -441,7 +443,7 @@ void TxtReaderActivity::renderPage() { break; } - renderer.drawText(cachedFontId, x, y, line.c_str()); + renderer.drawText(cachedFontId, x, y, line.c_str(), !inverse); } y += lineHeight; } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..4c03fa38 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -24,7 +24,7 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; -constexpr int readerSettingsCount = 9; +constexpr int readerSettingsCount = 10; const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), @@ -36,7 +36,8 @@ const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), - SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; + SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), + SettingInfo::Toggle("Inverse Display", &CrossPointSettings::inverseDisplay)}; constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = {