Merge branch 'master' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -63,7 +63,7 @@ bool CrossPointState::loadFromBinaryFile() {
|
||||
if (version >= 2) {
|
||||
serialization::readPod(inputFile, lastSleepImage);
|
||||
} else {
|
||||
lastSleepImage = 0;
|
||||
lastSleepImage = UINT8_MAX;
|
||||
}
|
||||
|
||||
if (version >= 3) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
@@ -8,7 +9,7 @@ class CrossPointState {
|
||||
|
||||
public:
|
||||
std::string openEpubPath;
|
||||
uint8_t lastSleepImage;
|
||||
uint8_t lastSleepImage = UINT8_MAX; // UINT8_MAX = unset sentinel
|
||||
uint8_t readerActivityLoadCount = 0;
|
||||
bool lastSleepFromReader = false;
|
||||
~CrossPointState() = default;
|
||||
|
||||
@@ -87,7 +87,7 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) {
|
||||
}
|
||||
|
||||
s.openEpubPath = doc["openEpubPath"] | std::string("");
|
||||
s.lastSleepImage = doc["lastSleepImage"] | (uint8_t)0;
|
||||
s.lastSleepImage = doc["lastSleepImage"] | (uint8_t)UINT8_MAX;
|
||||
s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0;
|
||||
s.lastSleepFromReader = doc["lastSleepFromReader"] | false;
|
||||
return true;
|
||||
|
||||
@@ -231,7 +231,7 @@ void SleepActivity::renderCustomSleepScreen() const {
|
||||
// Generate a random number between 1 and numFiles
|
||||
auto randomFileIndex = random(numFiles);
|
||||
// If we picked the same image as last time, reroll
|
||||
while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) {
|
||||
while (numFiles > 1 && APP_STATE.lastSleepImage != UINT8_MAX && randomFileIndex == APP_STATE.lastSleepImage) {
|
||||
randomFileIndex = random(numFiles);
|
||||
}
|
||||
APP_STATE.lastSleepImage = randomFileIndex;
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
#include <Epub/Page.h>
|
||||
#include <Epub/blocks/TextBlock.h>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <esp_system.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -19,6 +21,7 @@
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "QrDisplayActivity.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
@@ -27,7 +30,6 @@
|
||||
namespace {
|
||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||
constexpr unsigned long skipChapterMs = 700;
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
||||
|
||||
@@ -41,27 +43,6 @@ int clampPercent(int percent) {
|
||||
return percent;
|
||||
}
|
||||
|
||||
// Apply the logical reader orientation to the renderer.
|
||||
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
|
||||
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||
switch (orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EpubReaderActivity::onEnter() {
|
||||
@@ -73,7 +54,7 @@ void EpubReaderActivity::onEnter() {
|
||||
|
||||
// Configure screen orientation based on settings
|
||||
// NOTE: This affects layout math and must be applied before any render calls.
|
||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
epub->setupCacheDir();
|
||||
|
||||
@@ -183,13 +164,14 @@ void EpubReaderActivity::loop() {
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to file selection
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
activityManager.goToFileBrowser(epub ? epub->getPath() : "");
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press BACK goes directly to home (or restores position if viewing footnote)
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
if (footnoteDepth > 0) {
|
||||
restoreSavedPosition();
|
||||
return;
|
||||
@@ -198,20 +180,7 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
||||
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
||||
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
||||
const bool nextTriggered = usePressForPageTurn
|
||||
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
@@ -459,7 +428,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
||||
SETTINGS.saveToFile();
|
||||
|
||||
// Update renderer orientation to match the new logical coordinate system.
|
||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
// Reset section to force re-layout in the new orientation.
|
||||
section.reset();
|
||||
@@ -669,7 +638,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
const auto start = millis();
|
||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||
renderer.clearFontCache();
|
||||
}
|
||||
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
||||
|
||||
@@ -699,11 +667,30 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
|
||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
||||
const int orientedMarginRight, const int orientedMarginBottom,
|
||||
const int orientedMarginLeft) {
|
||||
const auto t0 = millis();
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
fcm->resetStats();
|
||||
|
||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||
const uint32_t heapBefore = esp_get_free_heap_size();
|
||||
auto scope = fcm->createPrewarmScope();
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass
|
||||
scope.endScanAndPrewarm();
|
||||
const uint32_t heapAfter = esp_get_free_heap_size();
|
||||
fcm->logStats("prewarm");
|
||||
const auto tPrewarm = millis();
|
||||
|
||||
LOG_DBG("ERS", "Heap: before=%lu after=%lu delta=%ld", heapBefore, heapAfter,
|
||||
(int32_t)heapAfter - (int32_t)heapBefore);
|
||||
|
||||
// Force special handling for pages with images when anti-aliasing is on
|
||||
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
||||
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderStatusBar();
|
||||
fcm->logStats("bw_render");
|
||||
const auto tBwRender = millis();
|
||||
|
||||
if (imagePageWithAA) {
|
||||
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
||||
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
|
||||
@@ -723,16 +710,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
}
|
||||
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
|
||||
} else if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
}
|
||||
const auto tDisplay = millis();
|
||||
|
||||
// Save bw buffer to reset buffer state after grayscale data sync
|
||||
renderer.storeBwBuffer();
|
||||
const auto tBwStore = millis();
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
@@ -741,20 +726,42 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
|
||||
// display grayscale part
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
}
|
||||
fcm->logStats("gray");
|
||||
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
} else {
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore,
|
||||
tEnd - t0);
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::renderStatusBar() const {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <CrossPointSettings.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
|
||||
namespace ReaderUtils {
|
||||
|
||||
constexpr unsigned long GO_HOME_MS = 1000;
|
||||
|
||||
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||
switch (orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct PageTurnResult {
|
||||
bool prev;
|
||||
bool next;
|
||||
};
|
||||
|
||||
inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
||||
const bool usePress = !SETTINGS.longPressChapterSkip;
|
||||
const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||
input.wasPressed(MappedInputManager::Button::Left))
|
||||
: (input.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||
input.wasReleased(MappedInputManager::Button::Power);
|
||||
const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn ||
|
||||
input.wasPressed(MappedInputManager::Button::Right))
|
||||
: (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn ||
|
||||
input.wasReleased(MappedInputManager::Button::Right));
|
||||
return {prev, next};
|
||||
}
|
||||
|
||||
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
}
|
||||
}
|
||||
|
||||
// Grayscale anti-aliasing pass. Renders content twice (LSB + MSB) to build
|
||||
// the grayscale buffer. Only the content callback is re-rendered — status bars
|
||||
// and other overlays should be drawn before calling this.
|
||||
// Kept as a template to avoid std::function overhead; instantiated once per reader type.
|
||||
template <typename RenderFn>
|
||||
void renderAntiAliased(GfxRenderer& renderer, RenderFn&& renderFn) {
|
||||
if (!renderer.storeBwBuffer()) {
|
||||
LOG_ERR("READER", "Failed to store BW buffer for anti-aliasing");
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
renderFn();
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
renderFn();
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
|
||||
renderer.restoreBwBuffer();
|
||||
}
|
||||
|
||||
} // namespace ReaderUtils
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "TxtReaderActivity.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
@@ -9,14 +10,13 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
||||
|
||||
// Cache file magic and version
|
||||
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
||||
@@ -88,23 +88,7 @@ void TxtReaderActivity::onEnter() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure screen orientation based on settings
|
||||
switch (SETTINGS.orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
txt->setupCacheDir();
|
||||
|
||||
@@ -134,31 +118,19 @@ void TxtReaderActivity::onExit() {
|
||||
|
||||
void TxtReaderActivity::loop() {
|
||||
// Long press BACK (1s+) goes to file selection
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press BACK goes directly to home
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
|
||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
||||
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
||||
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
||||
const bool nextTriggered = usePressForPageTurn
|
||||
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
@@ -316,7 +288,6 @@ void TxtReaderActivity::render(RenderLock&&) {
|
||||
|
||||
renderer.clearScreen();
|
||||
renderPage();
|
||||
renderer.clearFontCache();
|
||||
|
||||
// Save progress
|
||||
saveProgress();
|
||||
@@ -361,39 +332,22 @@ void TxtReaderActivity::renderPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// First pass: BW rendering
|
||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
auto scope = fcm->createPrewarmScope();
|
||||
renderLines(); // scan pass — text accumulated, no drawing
|
||||
scope.endScanAndPrewarm();
|
||||
|
||||
// BW rendering
|
||||
renderLines();
|
||||
renderStatusBar();
|
||||
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
}
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
|
||||
// Grayscale rendering pass (for anti-aliased fonts)
|
||||
if (SETTINGS.textAntiAliasing) {
|
||||
// Save BW buffer for restoration after grayscale pass
|
||||
renderer.storeBwBuffer();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
renderLines();
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
renderLines();
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
|
||||
// Restore BW buffer
|
||||
renderer.restoreBwBuffer();
|
||||
ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); });
|
||||
}
|
||||
// scope destructor clears font cache via FontCacheManager
|
||||
}
|
||||
|
||||
void TxtReaderActivity::renderStatusBar() const {
|
||||
|
||||
@@ -89,8 +89,13 @@ void SettingsActivity::loop() {
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
SETTINGS.saveToFile();
|
||||
onGoHome();
|
||||
if (selectedSettingIndex > 0) {
|
||||
selectedSettingIndex = 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
SETTINGS.saveToFile();
|
||||
onGoHome();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include <Epub.h>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalDisplay.h>
|
||||
@@ -32,6 +33,7 @@ MappedInputManager mappedInputManager(gpio);
|
||||
GfxRenderer renderer(display);
|
||||
ActivityManager activityManager(renderer, mappedInputManager);
|
||||
FontDecompressor fontDecompressor;
|
||||
FontCacheManager fontCacheManager(renderer.getFontMap());
|
||||
|
||||
// Fonts
|
||||
EpdFont bookerly14RegularFont(&bookerly_14_regular);
|
||||
@@ -203,7 +205,8 @@ void setupDisplayAndFonts() {
|
||||
if (!fontDecompressor.init()) {
|
||||
LOG_ERR("MAIN", "Font decompressor init failed");
|
||||
}
|
||||
renderer.setFontDecompressor(&fontDecompressor);
|
||||
fontCacheManager.setFontDecompressor(&fontDecompressor);
|
||||
renderer.setFontCacheManager(&fontCacheManager);
|
||||
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
|
||||
#ifndef OMIT_FONTS
|
||||
renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily);
|
||||
|
||||
+14
-2
@@ -1,5 +1,6 @@
|
||||
#include "QrUtils.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
#include <qrcode.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -12,7 +13,18 @@ void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const
|
||||
// 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();
|
||||
size_t len = textPayload.length();
|
||||
|
||||
// Truncate to max QR capacity at a UTF-8 safe boundary to avoid splitting multi-byte sequences
|
||||
static constexpr size_t MAX_QR_CAPACITY = 2953; // Version 40, ECC_LOW, byte mode
|
||||
std::string truncated;
|
||||
const char* payload = textPayload.c_str();
|
||||
if (len > MAX_QR_CAPACITY) {
|
||||
len = utf8SafeTruncateBuffer(textPayload.c_str(), static_cast<int>(MAX_QR_CAPACITY));
|
||||
truncated = textPayload.substr(0, len);
|
||||
payload = truncated.c_str();
|
||||
}
|
||||
|
||||
int version = 4;
|
||||
if (len > 114) version = 10;
|
||||
if (len > 395) version = 20;
|
||||
@@ -25,7 +37,7 @@ void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const
|
||||
|
||||
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());
|
||||
int8_t res = qrcode_initText(&qrcode, qrcodeBytes.get(), version, ECC_LOW, payload);
|
||||
|
||||
if (res == 0) {
|
||||
// Determine the optimal pixel size.
|
||||
|
||||
Reference in New Issue
Block a user