From 128eb614a6a424ef0bbb53dce8f97ebcc79520b8 Mon Sep 17 00:00:00 2001 From: Eliz Date: Wed, 25 Feb 2026 09:12:31 +0000 Subject: [PATCH] feat: Current page as QR (#1099) ## Summary * **What is the goal of this PR?** Implements QR text of the current page * **What changes are included?** ## Additional Context I saw this feature request at #982 It made sense to me so I implemented. But if the team thinks it is not necessary please let me know and we can close the PR. | Page | Menu | QR | |------|-------|----| | ![IMG_6601.bmp](https://github.com/user-attachments/files/25473201/IMG_6601.bmp) | ![IMG_6599.bmp](https://github.com/user-attachments/files/25473202/IMG_6599.bmp) | ![IMG_6600.bmp](https://github.com/user-attachments/files/25473205/IMG_6600.bmp) | --- ### AI Usage Did you use AI tools to help write this code? _** YES --------- Co-authored-by: Eliz Kilic --- lib/Epub/Epub/Page.h | 1 + lib/Epub/Epub/blocks/TextBlock.h | 1 + lib/I18n/translations/english.yaml | 1 + .../network/CrossPointWebServerActivity.cpp | 37 ++++--------- src/activities/reader/EpubReaderActivity.cpp | 34 ++++++++++++ .../reader/EpubReaderMenuActivity.h | 24 ++++++--- src/activities/reader/QrDisplayActivity.cpp | 45 ++++++++++++++++ src/activities/reader/QrDisplayActivity.h | 22 ++++++++ src/util/QrUtils.cpp | 54 +++++++++++++++++++ src/util/QrUtils.h | 14 +++++ 10 files changed, 199 insertions(+), 34 deletions(-) create mode 100644 src/activities/reader/QrDisplayActivity.cpp create mode 100644 src/activities/reader/QrDisplayActivity.h create mode 100644 src/util/QrUtils.cpp create mode 100644 src/util/QrUtils.h diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index af792f6f..9970baec 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -32,6 +32,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)) {} + const std::shared_ptr& getBlock() const { return block; } void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; bool serialize(FsFile& file) override; PageElementTag getTag() const override { return TAG_PageLine; } diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 11539044..9a1dad15 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -27,6 +27,7 @@ class TextBlock final : public Block { ~TextBlock() override = default; void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; } const BlockStyle& getBlockStyle() const { return blockStyle; } + const std::vector& getWords() const { return words; } bool isEmpty() override { return words.empty(); } // given a renderer works out where to break the words into lines void render(const GfxRenderer& renderer, int fontId, int x, int y) const; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index b3e0c08c..578a42b6 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -285,6 +285,7 @@ STR_GO_TO_PERCENT: "Go to %" STR_GO_HOME_BUTTON: "Go Home" STR_SYNC_PROGRESS: "Sync Progress" STR_DELETE_CACHE: "Delete Book Cache" +STR_DISPLAY_QR: "Show page as QR" STR_CHAPTER_PREFIX: "Chapter: " STR_PAGES_SEPARATOR: " pages | " STR_BOOK_PREFIX: "Book: " diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index beee4f71..7e77cbe4 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include @@ -16,6 +15,7 @@ #include "activities/network/CalibreConnectActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/QrUtils.h" namespace { // AP Mode configuration @@ -24,8 +24,8 @@ constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use constexpr const char* AP_HOSTNAME = "crosspoint"; constexpr uint8_t AP_CHANNEL = 1; constexpr uint8_t AP_MAX_CONNECTIONS = 4; -constexpr int QR_CODE_WIDTH = 6 * 33; -constexpr int QR_CODE_HEIGHT = 200; +constexpr int QR_CODE_WIDTH = 198; +constexpr int QR_CODE_HEIGHT = 198; // DNS server for captive portal (redirects all DNS queries to our IP) DNSServer* dnsServer = nullptr; @@ -363,28 +363,6 @@ void CrossPointWebServerActivity::render(Activity::RenderLock&&) { } } -void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data) { - // Implementation of QR code calculation - // The structure to manage the QR code - QRCode qrcode; - uint8_t qrcodeBytes[qrcode_getBufferSize(4)]; - LOG_DBG("WEBACT", "QR Code (%lu): %s", data.length(), data.c_str()); - - qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str()); - const uint8_t px = 6; // pixels per module - for (uint8_t cy = 0; cy < qrcode.size; cy++) { - for (uint8_t cx = 0; cx < qrcode.size; cx++) { - if (qrcode_getModule(&qrcode, cx, cy)) { - // Serial.print("**"); - renderer.fillRect(x + px * cx, y + px * cy, px, px, true); - } else { - // Serial.print(" "); - } - } - // Serial.print("\n"); - } -} - void CrossPointWebServerActivity::renderServerRunning() const { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); @@ -404,7 +382,8 @@ void CrossPointWebServerActivity::renderServerRunning() const { // Show QR code for Wifi const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; - drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig); + const Rect qrBoundsWifi(metrics.contentSidePadding, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT); + QrUtils::drawQrCode(renderer, qrBoundsWifi, wifiConfig); // Show network name renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, @@ -421,7 +400,8 @@ void CrossPointWebServerActivity::renderServerRunning() const { std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/"; // Show QR code for URL - drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl); + const Rect qrBoundsUrl(metrics.contentSidePadding, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT); + QrUtils::drawQrCode(renderer, qrBoundsUrl, hostnameUrl); // Show IP address as fallback renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, @@ -440,7 +420,8 @@ void CrossPointWebServerActivity::renderServerRunning() const { // Show QR code for URL std::string webInfo = "http://" + connectedIP + "/"; - drawQRCode(renderer, (pageWidth - QR_CODE_WIDTH) / 2, startY, webInfo); + const Rect qrBounds((pageWidth - QR_CODE_WIDTH) / 2, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT); + QrUtils::drawQrCode(renderer, qrBounds, webInfo); startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2; // Show web server URL prominently diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 31257ca9..357e1501 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1,6 +1,7 @@ #include "EpubReaderActivity.h" #include +#include #include #include #include @@ -14,6 +15,7 @@ #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.h" #include "MappedInputManager.h" +#include "QrDisplayActivity.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" @@ -403,6 +405,38 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction })); break; } + case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: { + if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { + auto p = section->loadPageFromSectionFile(); + if (p) { + std::string fullText; + for (const auto& el : p->elements) { + if (el->getTag() == TAG_PageLine) { + const auto& line = static_cast(*el); + if (line.getBlock()) { + const auto& words = line.getBlock()->getWords(); + for (const auto& w : words) { + if (!fullText.empty()) fullText += " "; + fullText += w; + } + } + } + } + if (!fullText.empty()) { + exitActivity(); + enterNewActivity(new QrDisplayActivity(renderer, mappedInput, fullText, [this]() { + exitActivity(); + requestUpdate(); + })); + break; + } + } + } + // If no text or page loading failed, just close menu + exitActivity(); + requestUpdate(); + break; + } case EpubReaderMenuActivity::MenuAction::GO_HOME: { // Defer go home to avoid race condition with display task pendingGoHome = true; diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 787a2302..f43bce6e 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -12,7 +12,16 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { public: // Menu actions available from the reader menu. - enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, SCREENSHOT, GO_HOME, SYNC, DELETE_CACHE }; + enum class MenuAction { + SELECT_CHAPTER, + GO_TO_PERCENT, + ROTATE_SCREEN, + SCREENSHOT, + DISPLAY_QR, + GO_HOME, + SYNC, + DELETE_CACHE + }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, @@ -39,11 +48,14 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { }; // Fixed menu layout (order matters for up/down navigation). - const std::vector menuItems = { - {MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}, {MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}, - {MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}, {MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}, - {MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}, {MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}, - {MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}}; + const std::vector menuItems = {{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}, + {MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}, + {MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}, + {MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}, + {MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}, + {MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}, + {MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}, + {MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}}; int selectedIndex = 0; ButtonNavigator buttonNavigator; diff --git a/src/activities/reader/QrDisplayActivity.cpp b/src/activities/reader/QrDisplayActivity.cpp new file mode 100644 index 00000000..95a04b52 --- /dev/null +++ b/src/activities/reader/QrDisplayActivity.cpp @@ -0,0 +1,45 @@ +#include "QrDisplayActivity.h" + +#include +#include + +#include "MappedInputManager.h" +#include "components/UITheme.h" +#include "fontIds.h" +#include "util/QrUtils.h" + +void QrDisplayActivity::onEnter() { + Activity::onEnter(); + requestUpdate(); +} + +void QrDisplayActivity::onExit() { Activity::onExit(); } + +void QrDisplayActivity::loop() { + if (mappedInput.wasReleased(MappedInputManager::Button::Back) || + mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + onGoBack(); + return; + } +} + +void QrDisplayActivity::render(Activity::RenderLock&&) { + renderer.clearScreen(); + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_DISPLAY_QR), nullptr); + + const int availableWidth = pageWidth - 40; + const int availableHeight = pageHeight - metrics.topPadding - metrics.headerHeight - metrics.verticalSpacing * 2 - 40; + const int startY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + + const Rect qrBounds(20, startY, availableWidth, availableHeight); + QrUtils::drawQrCode(renderer, qrBounds, textPayload); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/reader/QrDisplayActivity.h b/src/activities/reader/QrDisplayActivity.h new file mode 100644 index 00000000..f3958af7 --- /dev/null +++ b/src/activities/reader/QrDisplayActivity.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include + +#include "activities/Activity.h" + +class QrDisplayActivity final : public Activity { + public: + explicit QrDisplayActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& textPayload, + const std::function& onGoBack) + : Activity("QrDisplay", renderer, mappedInput), textPayload(textPayload), onGoBack(onGoBack) {} + + void onEnter() override; + void onExit() override; + void loop() override; + void render(Activity::RenderLock&&) override; + + private: + std::string textPayload; + const std::function onGoBack; +}; diff --git a/src/util/QrUtils.cpp b/src/util/QrUtils.cpp new file mode 100644 index 00000000..86b0a38e --- /dev/null +++ b/src/util/QrUtils.cpp @@ -0,0 +1,54 @@ +#include "QrUtils.h" + +#include + +#include +#include + +#include "Logging.h" + +void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const std::string& textPayload) { + // Dynamically calculate the QR code version based on text length + // Version 4 holds ~114 bytes, Version 10 ~395, Version 20 ~1066, up to 40 + // qrcode.h max version is 40. + // Formula: approx version = size / 26 + 1 (very rough estimate, better to find best fit) + const size_t len = textPayload.length(); + int version = 4; + if (len > 114) version = 10; + if (len > 395) version = 20; + if (len > 1066) version = 30; + if (len > 2110) version = 40; + + // Make sure we have a large enough buffer on the heap to avoid blowing the stack + uint32_t bufferSize = qrcode_getBufferSize(version); + auto qrcodeBytes = std::make_unique(bufferSize); + + QRCode qrcode; + // Initialize the QR code. We use ECC_LOW for max capacity. + int8_t res = qrcode_initText(&qrcode, qrcodeBytes.get(), version, ECC_LOW, textPayload.c_str()); + + if (res == 0) { + // Determine the optimal pixel size. + const int maxDim = std::min(bounds.width, bounds.height); + + int px = maxDim / qrcode.size; + if (px < 1) px = 1; + + // Calculate centering X and Y + const int qrDisplaySize = qrcode.size * px; + const int xOff = bounds.x + (bounds.width - qrDisplaySize) / 2; + const int yOff = bounds.y + (bounds.height - qrDisplaySize) / 2; + + // Draw the QR Code + for (uint8_t cy = 0; cy < qrcode.size; cy++) { + for (uint8_t cx = 0; cx < qrcode.size; cx++) { + if (qrcode_getModule(&qrcode, cx, cy)) { + renderer.fillRect(xOff + px * cx, yOff + px * cy, px, px, true); + } + } + } + } else { + // If it fails (e.g. text too large), log an error + LOG_ERR("QR", "Text too large for QR Code version %d", version); + } +} diff --git a/src/util/QrUtils.h b/src/util/QrUtils.h new file mode 100644 index 00000000..01eced76 --- /dev/null +++ b/src/util/QrUtils.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +#include "components/themes/BaseTheme.h" + +namespace QrUtils { + +// Renders a QR code with the given text payload within the specified bounding box. +void drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const std::string& textPayload); + +} // namespace QrUtils