167 Commits

Author SHA1 Message Date
cottongin
255b98bda0 port: upstream PRs #1311 (inter-word spacing fix) and #1322 (zip early exit)
PR #1311: Replace separate spaceWidth + getSpaceKernAdjust() with a
single getSpaceAdvance() that combines space glyph advance and kerning
in fixed-point before snapping to pixels, eliminating +/-1 px rounding
drift in text layout.

PR #1322: Add early exit to fillUncompressedSizes() once all target
entries are matched, avoiding unnecessary central directory traversal.

Also updates tracking docs and verifies PR #1329 (reader utils refactor)
matches upstream after merge.

Made-with: Cursor
2026-03-08 15:53:13 -04:00
cottongin
0d8a3fdbdd fix: restore preferred orientation settings and long-press sub-menu
Re-add DynamicEnum entries for preferredPortrait/preferredLandscape in
Settings with JSON persistence. Restore long-press Confirm on the
reader menu's orientation toggle to open an inline sub-menu with all
4 orientation options.

Made-with: Cursor
2026-03-08 06:18:17 -04:00
cottongin
5ca72ef231 docs: add chat summary for three reader bug fixes
Made-with: Cursor
2026-03-08 06:04:25 -04:00
cottongin
422cad7bc5 fix: resolve three reader bugs (confirm eaten, footnotes menu, phantom render)
1. Clear ignoreNextConfirmRelease after transferring state to child
   activity, so the next Confirm press isn't silently consumed.
2. Add conditional FOOTNOTES entry to reader menu when the current
   book has footnotes.
3. Guard clock-minute requestUpdate() with !isReaderActivity() to
   prevent full e-ink re-renders every minute while reading.

Made-with: Cursor
2026-03-08 05:56:10 -04:00
cottongin
1b628a9223 Merge branch 'port/1325-settings-label' into mod/master 2026-03-08 05:14:49 -04:00
cottongin
633d56195a Merge branch 'port/1320-jpeg-cleanup' into mod/master 2026-03-08 05:13:47 -04:00
cottongin
83aa38d1a2 docs: update tracking for ported PRs #1329, #1143, #1172, #1320, #1325
Add detailed entries to MERGED.md for all 5 ported PRs with context,
changes applied, and differences from upstream. Update upstream-sync.md
tracking table with new entries and sync date.

Made-with: Cursor
2026-03-08 05:02:05 -04:00
cottongin
7fe093b57a feat: add multi-spine chapter caching for seamless cross-spine navigation
When loading a section, proactively indexes all spine items belonging
to the same TOC chapter so page-turning across spine boundaries within
a chapter is instant. Uses Section::readCachedPageCount() to skip
already-cached sections and shows an "Indexing (x/y)" progress popup.

Ported from upstream PR #1172, adapted to mod architecture.

Made-with: Cursor
2026-03-08 04:59:26 -04:00
cottongin
867faad916 feat: add TOC-aware navigation to EpubReaderActivity
Long-press chapter skip now walks by TOC entries instead of spine
indices, enabling finer navigation in books with multi-chapter spines.

Status bar chapter title now uses section-level getTocIndexForPage()
for accurate subchapter display. Chapter selection passes tocIndex
back so the reader can jump directly to the right page within a spine.

Add pendingTocIndex to EpubReaderActivity for deferred cross-spine
TOC navigation, resolved after the target section loads.

Ported from upstream PRs #1143 and #1172, adapted to mod architecture.

Made-with: Cursor
2026-03-08 04:57:08 -04:00
cottongin
f2a2b03074 feat: add TOC boundary API and anchor page breaks to Section
Extend Section with TOC boundary tracking: buildTocBoundaries(),
getTocIndexForPage(), getPageForTocIndex(), getPageRangeForTocIndex(),
readAnchorMap(), and readCachedPageCount() for lightweight cache queries.

ChapterHtmlSlimParser now accepts a tocAnchors set and forces page breaks
at TOC anchor boundaries so each chapter starts on a fresh page.

Increment SECTION_FILE_VERSION to 19 for new TOC boundary data.

Ported from upstream PRs #1143 and #1172, adapted to mod architecture.

Made-with: Cursor
2026-03-08 04:49:43 -04:00
cottongin
e43f763201 port: dynamic settings tab label (upstream PR #1325)
Adapted from upstream PR #1325 (not yet merged).
When focused on the tab bar (selectedSettingIndex == 0), the confirm
button label now shows the name of the next category instead of
the generic "Toggle" text.

If/when #1325 is merged upstream, this commit should be dropped
during the next sync and the upstream version used instead.

Made-with: Cursor
2026-03-08 04:42:34 -04:00
cottongin
ad843d8edc port: RAII jpeg resource cleanup (upstream PR #1320)
Adapted from upstream PR #1320 (not yet merged).
Replaces scattered free()/delete cleanup with RAII Cleanup struct
that guarantees resource release on all return paths. Changes
rowCount from uint16_t* to uint32_t* to prevent overflow on
large images.

If/when #1320 is merged upstream, this commit should be dropped
during the next sync and the upstream version used instead.

Made-with: Cursor
2026-03-08 04:40:16 -04:00
cottongin
0d828ba986 port: extract shared reader utilities (upstream PR #1329)
Adapted from upstream PR #1329 (not yet merged).
Adds ReaderUtils.h with shared orientation, page-turn detection,
refresh cycle, and anti-aliased rendering utilities. Refactors
EpubReaderActivity and TxtReaderActivity to use shared implementations
instead of duplicated inline code.

If/when #1329 is merged upstream, this commit should be dropped
during the next sync and the upstream version used instead.

Made-with: Cursor
2026-03-08 04:37:13 -04:00
cottongin
cc90d7c755 merge: replace mod/master with mod/master-resync (upstream sync checkpoint)
Full upstream resync: mod/master-resync was rebuilt from upstream/master
with all mod features re-applied to the new ActivityManager architecture.
This merge records the history connection but keeps the resync content.
2026-03-08 04:17:33 -04:00
cottongin
dac087f391 docs: add chat summaries for upstream sync and bug fix sessions
Made-with: Cursor
2026-03-08 04:14:56 -04:00
cottongin
022f5197d7 fix: placeholder cover text, indexing timing, TOC long-press, cache deletion UI
- Fix fp4 fixed-point misuse in PlaceholderCoverGenerator (advanceX is 12.4
  fixed-point, not pixels) causing only first letter of each word to render
- Remove duplicate silentIndexNextChapterIfNeeded() call from loop() that
  blocked UI before render, preventing the indexing indicator from showing
- Fix indexing icon Y position to align within the status bar
- Add ignoreNextConfirmRelease to EpubReaderChapterSelectionActivity so
  long-press confirm release doesn't immediately select the first TOC item
- Reload recent books after cache deletion in HomeActivity and clear stale
  ignoreNextConfirmRelease flag to fix "no open books" and double-press bugs

Made-with: Cursor
2026-03-07 22:58:13 -05:00
cottongin
a5ca15df4f feat: restore book cover/thumbnail prerender on first open
- Add isValidThumbnailBmp(), generateInvalidFormatCoverBmp(), and
  generateInvalidFormatThumbBmp() methods to Epub class for validating
  BMP files and generating X-pattern marker images when cover extraction
  fails (e.g., progressive JPG).
- Restore prerender block in EpubReaderActivity::onEnter() that checks
  for missing cover BMPs (fit + cropped) and thumbnail BMPs at each
  PRERENDER_THUMB_HEIGHTS size, showing a "Preparing book..." popup
  with progress. Falls back to PlaceholderCoverGenerator, then to
  invalid-format marker BMPs as last resort.

Made-with: Cursor
2026-03-07 21:22:19 -05:00
cottongin
22c189281c feat: restore settings order and silent indexing display options
- Move Letterbox Fill setting to immediately after Sleep Screen Cover
  Filter in the Display section where it is more relevant.
- Add Indexing Display setting to Display section with three modes:
  Popup (default), Status Bar Text ("Indexing..."), and Status Bar Icon
  (hourglass).
- Restore silent indexing logic in EpubReaderActivity::render() that
  proactively indexes the next chapter when on a text-only page near
  the end of the current chapter (non-popup mode only).
- Draw indexing indicator in renderStatusBar() when silentIndexingActive
  is set and the user has chosen text or icon mode.

Made-with: Cursor
2026-03-07 21:18:09 -05:00
cottongin
0493f300be feat: restructure reader menu with submenus and long-press TOC
- Replace scattered book management actions (Archive, Delete, Reindex,
  Delete Cache) with single "Manage Book" entry that opens
  BookManageMenuActivity as a submenu.
- Replace scattered dictionary actions (Lookup Word, Lookup History,
  Delete Dict Cache) with single "Dictionary" entry that opens new
  DictionaryMenuActivity submenu.
- Add long-press Confirm (700ms) to open Table of Contents directly
  from the reader, bypassing the menu.
- Add STR_DICTIONARY i18n key and regenerate I18nKeys.h/I18nStrings.h.

Made-with: Cursor
2026-03-07 21:12:09 -05:00
cottongin
f44657aeba fix: restore clock display and fix placeholder cover generation
- Add clock rendering to BaseTheme::drawHeader() and LyraTheme::drawHeader()
  after battery, before title. Respects clockFormat (OFF/AM-PM/24H) and
  clockSize (Small/Medium/Large) settings.
- Fix PlaceholderCoverGenerator splitWords() to treat newlines, tabs, and
  carriage returns as whitespace delimiters (not just spaces), preventing
  one-character-per-line output from EPUB metadata with embedded newlines.
- Remove drawBorder() from placeholder covers since the UI already draws
  its own frame around book cards.

Made-with: Cursor
2026-03-07 21:00:23 -05:00
cottongin
4627ec95f9 fix: resolve mod build errors after upstream sync
- Update open-x4-sdk submodule to 9f76376 (BatteryMonitor ESP-IDF 5.x compat)
- Add RTC_NOINIT bounds check for logHead in Logging.cpp
- Add drawTextRotated90CCW to GfxRenderer for dictionary UI
- Add getWordXpos() accessor to TextBlock for dictionary word selection
- Fix bare include paths (ActivityResult.h, RenderLock.h) across 10 files
- Fix rvalue ref binding in setResult() lambdas (std::move pattern)
- Fix std::max type mismatch (uint8_t vs int) in EpubReaderActivity
- Fix FsFile forward declaration conflict in Dictionary.h
- Restore StringUtils::checkFileExtension() and sortFileList()
- Restore RecentBooksStore::removeBook()

Made-with: Cursor
2026-03-07 20:56:40 -05:00
cottongin
9464df1727 mod: restore missing mod features from resync audit
Re-add KOReaderSyncActivity PUSH_ONLY mode (PR #1090):
- SyncMode enum with INTERACTIVE/PUSH_ONLY, deferFinish pattern
- Push & Sleep menu action in EpubReaderMenuActivity
- ActivityManager::requestSleep() for activity-initiated sleep
- main.cpp checks isSleepRequested() each loop iteration

Wire EndOfBookMenuActivity into EpubReaderActivity:
- pendingEndOfBookMenu deferred flag avoids render-lock deadlock
- Handles all 6 actions: ARCHIVE, DELETE, TABLE_OF_CONTENTS,
  BACK_TO_BEGINNING, CLOSE_BOOK, CLOSE_MENU

Add book management to reader menu:
- ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK actions with handlers

Port silent next-chapter pre-indexing:
- silentIndexNextChapterIfNeeded() proactively indexes next chapter
  when user is near end of current one, eliminating load screens

Add per-book letterbox fill toggle in reader menu:
- LETTERBOX_FILL cycles Default/Dithered/Solid/None
- Loads/saves per-book override via BookSettings
- bookCachePath constructor param added to EpubReaderMenuActivity

Made-with: Cursor
2026-03-07 16:53:17 -05:00
cottongin
60a3e21c0e mod: Phase 3 — Re-port unmerged upstream PRs
Re-applied upstream PRs not yet merged to upstream/master:

- #1055: Byte-level framebuffer writes (fillPhysicalHSpan*,
  optimized fillRect/drawLine/fillRectDither/fillPolygon)
- #1027: Word-width cache (FNV-1a, 128-entry) and hyphenation
  early exit in ParsedText for 7-9% layout speedup
- #1068: Already present in upstream — URL hyphenation fix
- #1019: Already present in upstream — file extensions in browser
- #1090/#1185/#1217: KOReader sync improvements — binary credential
  store, document hash caching, ChapterXPathIndexer integration
- #1209: OPDS multi-server — OpdsBookBrowserActivity accepts
  OpdsServer, directory picker for downloads, download-complete
  prompt with open/back options
- #857: Dictionary activities already ported in Phase 1/2
- #1003: Placeholder cover already integrated in Phase 2

Also fixed: STR_OFF i18n string, include paths, replaced
Epub::isValidThumbnailBmp with Storage.exists, replaced
StringUtils::checkFileExtension with FsHelpers equivalents.

Made-with: Cursor
2026-03-07 16:15:42 -05:00
cottongin
30473c27d3 mod: Phase 2c-e — GfxRenderer, themes, SleepActivity, SettingsActivity, platformio
- Add drawPixelGray to GfxRenderer for letterbox fill rendering
- Add PRERENDER_THUMB_HEIGHTS to UITheme for placeholder cover generation
- Add [env:mod] build environment to platformio.ini
- Implement sleep screen letterbox fill (solid/dithered) with edge
  caching in SleepActivity, including placeholder cover fallback
- Add Clock settings category to SettingsActivity with timezone,
  NTP sync, and set-time actions; replace CalibreSettings with
  OpdsServerListActivity; add DynamicEnum rendering support
- Add long-press book management to RecentBooksActivity

Made-with: Cursor
2026-03-07 15:52:46 -05:00
cottongin
d1ee45592e chore: remove temporary diff file
Made-with: Cursor
2026-03-07 15:39:08 -05:00
cottongin
bd2cea8b8d mod: Phase 2b - adapt HomeActivity, EpubReaderMenuActivity, EpubReaderActivity
HomeActivity: Add mod features on top of upstream ActivityManager pattern:
- Multi-server OPDS support (OpdsServerStore instead of single URL)
- Long-press recent book for BookManageMenuActivity
- Long-press Browse Files to open archive folder
- Placeholder cover generation for books without covers
- startActivityForResult pattern for manage menu

EpubReaderMenuActivity: Replace upstream menu items with mod menu:
- Add/Remove Bookmark, Lookup Word, Go to Bookmark, Lookup History
- Table of Contents, Toggle Orientation, Toggle Font Size
- Close Book, Delete Dictionary Cache
- Pass isBookmarked and currentFontSize to constructor
- Show current orientation/font size value inline

EpubReaderActivity: Add mod action handlers:
- Bookmark add/remove via BookmarkStore
- Go to Bookmark via EpubReaderBookmarkSelectionActivity
- Dictionary word lookup via DictionaryWordSelectActivity
- Lookup history via LookedUpWordsActivity
- Delete dictionary cache
- Font size toggle with section re-layout
- Close Book action

ActivityResult: Add fontSize field to MenuResult
Made-with: Cursor
2026-03-07 15:38:53 -05:00
cottongin
66a754dabd mod: Phase 2a - add mod settings, I18n strings, and main.cpp integration
CrossPointSettings: Add mod-specific enums and fields:
- Clock: CLOCK_FORMAT, CLOCK_SIZE, TIMEZONE, clockFormat, clockSize,
  timezone, timezoneOffsetHours, autoNtpSync
- Sleep: SLEEP_SCREEN_LETTERBOX_FILL, sleepScreenLetterboxFill
- Reader: preferredPortrait, preferredLandscape
- Indexing: INDEXING_DISPLAY, indexingDisplay
- getTimezonePosixStr() for POSIX TZ string generation

main.cpp: Integrate mod initialization:
- OPDS store loading, boot NTP sync, timezone application
- Clock refresh loop (re-render on minute change)
- RTC time logging on boot

SettingsList.h: Add clock, timezone, and letterbox fill settings
JsonSettingsIO.cpp: Handle int8_t timezoneOffsetHours separately
I18n: Add ~80 mod string keys (english.yaml + regenerated I18nKeys.h)

Made-with: Cursor
2026-03-07 15:14:35 -05:00
cottongin
dfbc931c14 mod: Phase 1 - bring forward mod-exclusive files with ActivityManager migration
Brings ~55 mod-exclusive files to the upstream-based mod/master-resync branch:

Activities (migrated to new ActivityManager pattern):
- Clock/Time: SetTimeActivity, SetTimezoneOffsetActivity, NtpSyncActivity
- Dictionary: DictionaryDefinitionActivity, DictionarySuggestionsActivity,
  DictionaryWordSelectActivity, LookedUpWordsActivity
- Bookmark: EpubReaderBookmarkSelectionActivity
- Book management: BookManageMenuActivity, EndOfBookMenuActivity
- OPDS: OpdsServerListActivity, OpdsSettingsActivity
- Utility: DirectoryPickerActivity, NumericStepperActivity

Utilities (unchanged):
- BookManager, BookSettings, BookmarkStore, BootNtpSync
- Dictionary, LookupHistory, TimeSync, OpdsServerStore

Libraries: PlaceholderCover, TableData, ChapterXPathIndexer
Scripts: inject_mod_version, generate_book_icon, preview_placeholder_cover
Docs: KOReader sync XPath mapping

Migration changes:
- ActivityWithSubactivity -> Activity base class
- Callback constructors -> finish()/setResult() pattern
- enterNewActivity() -> startActivityForResult()
- Activity::RenderLock&& -> RenderLock&&

These files won't compile yet - they reference mod settings and I18n
strings that will be added in subsequent phases.

Made-with: Cursor
2026-03-07 15:10:00 -05:00
cottongin
bf604add85 fix skills 2026-03-07 13:56:15 -05:00
Jonasz Potoniec
170cc25774 chore: Polish localization for STR_DELETE (#1323) 2026-03-06 21:22:52 -05:00
Xuan-Son Nguyen
c40e92e4d1 fix: dump crash log without usb plugged, bump release log to INFO (#1332)
## Summary

Follow-up
https://github.com/crosspoint-reader/crosspoint-reader/pull/1145

- Fix log not being record without USB connected
- Bump release log to INFO for more logging details

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-06 22:05:23 +01:00
Uri Tauber
4d22256745 feat: footnote anchor navigation (#1245)
## Summary: Enable footnote anchor navigation in EPUB reader

This PR extracts the core anchor-to-page mapping mechanism from PR #1143
(TOC fragment navigation) to provide immediate footnote navigation
support. By merging this focused subset first, users get a complete
footnote experience now while simplifying the eventual review and merge
of the full #1143 PR.

  ---

## What this extracts from PR #1143

PR #1143 implements comprehensive TOC fragment navigation for EPUBs with
multi-chapter spine files. This PR takes only the anchor resolution
infrastructure:

- Anchor-to-page mapping in section cache: During page layout,
ChapterHtmlSlimParser records which page each HTML id attribute lands
on, serializing the map into the .bin cache file.
- Anchor resolution in `EpubReaderActivity`: When navigating to a
footnote link with a fragment (e.g., `chapter2.xhtml#note1`), the reader
resolves the anchor to a page number and jumps directly to it.
- Section file format change: Bumped to version 15, adds anchor map
offset in header.

  ---

## Simplified scope vs. PR #1143

To minimize conflicts and complexity, this PR differs from #1143 in key
ways:

* **Anchors tracked**
  * **Origin:** Only TOC anchors (passed via `std::set`)
  * **This branch:** All `id` attributes

* **Page breaks**
  * **Origin**: Forces new page at TOC chapter boundaries
  * **This branch:** None — natural flow

* **TOC integration**
  * **Origin**: `tocBoundaries`, `getTocIndexForPage()`, chapter skip
  * **This branch:** None — just footnote links

* **Bug fix**
  * **This branch:** Fixed anchor page off-by-1/2 bug


The anchor recording bug (recording page number before `makePages()`
flushes previous block) was identified and fixed during this extraction.
The fix uses a deferred `pendingAnchorId` pattern that records the
anchor after page completion.

  ---

## Positioning for future merge

Changes are structured to minimize conflicts when #1143 eventually
merges:

- `ChapterHtmlSlimParser.cpp` `startElement()`: Both branches rewrite
the same if `(!idAttr.empty())` block. The merged version will combine
both approaches (TOC anchors get page breaks + immediate recording;
footnote anchors get deferred recording).
- `EpubReaderActivity.cpp` `render()`: The `pendingAnchor` resolution
block is positioned at the exact same insertion point where #1143 places
its `pendingTocIndex` block (line 596, right after `nextPageNumber`
assignment). During merge, both blocks will sit side-by-side.
   
  ---

## Why merge separately?

1. Immediate user value: Footnote navigation works now without waiting
for the full TOC overhaul
   2. Easier review: ~100 lines vs. 500+ lines in #1143 
3. Bug fix included: The page recording bug is fixed here and will carry
into #1143
4. Minimal conflicts: Structured for clean merge — both PRs touch the
same files but in complementary ways
---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_ Done by
Claude Opus 4.6
2026-03-06 21:10:45 +03:00
Xuan-Son Nguyen
18b36efbae feat: dump crash report to sdcard (#1145)
## Summary

This allow dumping crash message (i.e. assertion fail) and stack trace
to `crash_report.txt` file on sdcard. The stack trace can then be
decoded using https://esphome.github.io/esp-stacktrace-decoder/

Could be useful to debug things like
https://github.com/crosspoint-reader/crosspoint-reader/issues/1137 where
error doesn't always happen.

May also be useful to show a screen to tell what happen (show on next
boot after crash), similar to [flipper zero crash
message](https://www.reddit.com/r/flipperzero/comments/10f8m3f/anyone_who_can_tell_me_why_this_message_pops_up/)
, but this is better to be a dedicated PR (I'm missing the
`drawTextWrapped` function, too lazy to code it ; update: exactly what I
need in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1141)

To test this:
- Option 1: add an `assert(false)` somewhere in the code
- Option 2: try dereferencing a nullptr
- Option 3: try `throw` an exception

Example of a crash report:

```
CrossPoint version: 1.1.0-dev

Panic reason: abort() was called at PC 0x4214585b on core 0

Recent logs:
[196] [DBG] [GFX] Time = 2 ms from clearScreen to displayBuffer
[1831] [DBG] [RBS] Recent books loaded from file (7 entries)
[1832] [DBG] [ACT] Exiting activity: Boot
[1832] [DBG] [ACT] Entering activity: Home
[1891] [DBG] [GFX] Time = 54 ms from clearScreen to displayBuffer
[2521] [DBG] [GFX] Time = 46 ms from clearScreen to displayBuffer
[4839] [DBG] [PWR] Going to low-power mode
[10048] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[20060] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[30072] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[34453] [DBG] [PWR] Restoring normal CPU frequency
[34485] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[35182] [DBG] [GFX] Time = 31 ms from clearScreen to displayBuffer
[36675] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[38800] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[40079] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes


Stack memory:
0x3FCB0650: 0x00000000 0x00000000 0x3FCB0668 0x4038DBB6 0x00000000 0x00000000 0x3FCA0030 0x3FC936D0 
0x3FCB0670: 0x3FCB067C 0x3FC936EC 0x3FCB0668 0x34313234 0x62353835 0x00000000 0x726F6261 0x20292874 
0x3FCB0690: 0x20736177 0x6C6C6163 0x61206465 0x43502074 0x34783020 0x35343132 0x20623538 0x63206E6F 
0x3FCB06B0: 0x2065726F 0x00000030 0x3FCA0000 0xB37A603F 0x00000001 0x3FCA7000 0x3FCABCDC 0x4214585E 
0x3FCB06D0: 0x3FCA7000 0x3FCA7000 0x3FCABCDC 0x421458AA 0x3FCABCDC 0x3FCA7000 0x3FCABCDC 0x421459CC 
0x3FCB06F0: 0x3FCA7000 0x3FCA7000 0x42145D5A 0x3C205624 0x40388560 0x3FCA7000 0x3FCABCFC 0x42079866 
0x3FCB0710: 0x3FCA7000 0x3FCA7000 0x00009C9A 0x4207B7F6 0x3FCA7000 0x42090000 0x001B7740 0x00000001 
0x3FCB0730: 0x3FCA7000 0x3FCA7000 0x00000001 0x600C0028 0x00000001 0x3FCA1000 0x00000000 0x00000000 
0x3FCB0750: 0x00000000 0x00000000 0x00000000 0xB37A603F 0x00000000 0x00000000 0x00000000 0x00000000 
0x3FCB0770: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x42090000 0x3FCA7000 0x4208F9C4 
0x3FCB0790: 0x00000000 0x00000000 0x00000000 0x40388368 0x00000000 0x00000000 0x00000000 0x00000000 
0x3FCB07B0: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 
0x3FCB07D0: 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xBAAD5678 0xDA6D3601 0x5EB5B9C5 0x2602E480 
0x3FCB07F0: 0x2BCDD33F 0x15556D4A 0x1F2140A0 0x5D59BEE3 0x8E76449F 0x6FB2D0CE 0xF5F46FAC 0x0112946A 
0x3FCB0810: 0x3B0B32E0 0x7A52B537 0x46801DB4 0xDA85DF9F 0x37E83D20 0x12861028 0x47A702BB 0x287A3C8A 
0x3FCB0830: 0x03632209 0xD44C5489 0x5E258453 0xFDA77529 0xE6748E23 0xADCF1394 0x67AD6778 0x2C208663 
0x3FCB0850: 0xC7985786 0xD4AA3AB2 0x312E1760 0xEC7AEAAE 0x1857020E 0x48003E7E 0xD6CB8763 0x9B4A3F66 
0x3FCB0870: 0x4B79E9F6 0xCBF739F0 0x3794C641 0xD0DBA3CB 0x95B9BE15 0x581C9983 0xDE62EFB6 0x20C67C5B 
0x3FCB0890: 0x1E4A3DF3 0xFB317C74 0xC0D86103 0x1D79ED56 0x72FE0862 0x3D38B0C8 0xD27EB587 0x0E0A4C40 
0x3FCB08B0: 0xF643ADC0 0x56D114D7 0x703AF879 0xAC7F3075 0x89C78C23 0xEDA86814 0xF767B3E3 0x0528838F 
0x3FCB08D0: 0x50ED4662 0x11FD38E7 0x8A5A83BB 0x658159BD 0x781AF696 0x8A700F79 0x526DDE23 0xC8472505 
0x3FCB08F0: 0x21AACC02 0xCB89369E 0xB82E5BE2 0x4C6C9D7D 0x9E724D9B 0xDC1067F7 0x84478FBC 0x4E89C444 
0x3FCB0910: 0x973F4229 0x49F93DA8 0xE30200F6 0xD1B5C391 0x8363A89F 0x2409E74C 0x3AFF7B52 0xCBEC2349 
0x3FCB0930: 0xD38F6695 0xBC3EA980 0xF067EBB1 0x7F87D167 0x92B3823B 0x9F0617D7 0xA7537C57 0x12CAB3D4 
0x3FCB0950: 0xC82EEE37 0x84D4B4BC 0xE1E2261C 0x488F0ADA 0x96EAF2FF 0x0BC493A0 0xCE614467 0x3829053D 
0x3FCB0970: 0xA41156BE 0x2747B77D 0x64DEA90B 0xE704AB0A 0xE4B01006 0x8D51903C 0x56CD3CF2 0x07E0A8E8 
0x3FCB0990: 0xD1DE05CE 0x33368522 0xD1889988 0x3A3097F4 0xB0796D09 0xC78948AA 0x6DEFC56E 0xD5C2E1D9 
0x3FCB09B0: 0xFD6DD8FA 0xA957B675 0xC202D80D 0x733FF8F4 0xA1484913 0x0B9AFBA6 0x330C07EA 0x2C09AD4C 
0x3FCB09D0: 0x3B1E08F7 0x3FCAE7D0 0x00000170 0xABBA1234 0x0000015C 0x3FCB00E0 0x00009C93 0x3FCA13C4 
0x3FCB09F0: 0x3FCA13C4 0x3FCB09E4 0x3FCA13BC 0x00000018 0x00000000 0x00000000 0x3FCB09E4 0x00000000 
0x3FCB0A10: 0x00000001 0x3FCAE7E0 0x706F6F6C 0x6B736154 0x00000000 0x00000000 0x3FCB07D0 0x00000005 
0x3FCB0A30: 0x00000000 0x00000001 0x00000000 0x3FCAB444 0x4209AFF0 0x0017E38F 0x00000000 0x3FCA7BD0 

```

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-06 17:46:13 +01:00
jpirnay
a35f372e1b fix: avoid zip filename overflow (#1321)
## Summary

* **What is the goal of this PR?** Potential stack buffer overflow from
untrusted ZIP entry name length
* **What changes are included?** If nameLen >= 256 , this writes past
the stack buffer. Risk: memory corruption/crash on malformed EPUB/ZIP.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** PARTIALLY **_ Issue
identified by AI
2026-03-05 21:25:17 -06:00
Baris Albayrak
4ef433e373 feat: add turkish translation (#1192)
Description
  This Pull Request introduces Turkish language support to CrossPoint
  Reader firmware.


  Key Changes:
- Translation File: Added lib/I18n/translations/turkish.yaml with 315
translated
     string keys, covering all system UI elements.
- I18N Script Update: Modified scripts/gen_i18n.py to include the "TR"
     abbreviation mapping for Turkish.
- System Integration: Regenerated I18N C++ files to include the new
Language::TR
     enum and STRINGS_TR array.
- UI Availability: The language is now selectable in the Settings menu
and
     correctly handles Turkish-specific characters (ç, ğ, ı, ö, ş, ü).
- Documentation: Updated docs/i18n.md to include Turkish in the list of
     supported languages.

  Testing:
   - Verified the build locally with PlatformIO.
- Flashed the firmware to an Xteink X4 device and confirmed the Turkish
UI
     renders correctly.
---

### AI Usage

Did you use AI tools to help write this code?  Yes Gemini

---------

Co-authored-by: Baris Albayrak <baris@Bariss-MacBook-Pro.local>
Co-authored-by: Barış Albayrak <barisa@pop-os.lan>
2026-03-05 18:24:44 -06:00
Stefan Blixten Karlsson
a5d7e03f54 fix: improve and add Swedish translations (#1317)
## Summary

* **What is the goal of this PR?**
  * Improve and add the latest missing Swedish translations.

* **What changes are included?**
* Added missing Swedish translations in
`lib\I18n\translations\swedish.yaml`
  
## Additional Context

* (none)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-03-05 14:25:29 -06:00
ariel-lindemann
047b0029c9 chore: add missing translations for Romanian (#1265)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

added Romanian ranslations from recent commits.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-03-05 10:19:17 -06:00
Zach Nelson
c3f1dbfa09 perf: Avoid creating strings for file extension checks (#1303)
## Summary

**What is the goal of this PR?**

This change avoids the pattern of creating a `std::string` using
`.substr` in order to compare against a file extension literal.
```c++
std::string path;
if (path.length() >= 4 && path.substr(path.length() - 4) == ".ext")
```

The `checkFileExtension` utility has moved from StringUtils to
FsHelpers, to be available to code in lib/. The signature now accepts a
`std::string_view` instead of `std::string`, which makes the single
implementation reusable for Arduino `String`.

Added utility functions for commonly repeated extensions.

These changes **save about 2 KB of flash (5,999,427 to 5,997,343)**.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-03-05 10:12:22 -06:00
Zach Nelson
ea88797c8e chore: Image settings Polish localization (#1299)
## Summary

**What is the goal of this PR?**

Quick follow up to #1291, adding Polish translations suggested by
@th0m4sek

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-03-05 19:08:36 +03:00
pablohc
218201bd1f fix: Correct relative file paths in SKILL.md documentation (#1304)
## Summary

* **What is the goal of this PR?**
Update relative paths to correctly navigate from .skills/ directory to
project root by adding ../ prefix to file references.

* **What changes are included?**
.skills/SKILL.md

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
2026-03-05 08:36:53 -06:00
Àngel
6ee05b08a1 chore: add missing Catalan strings (#1302)
## Summary

* **What is the goal of this PR?**
Add missing Catalan strings.

* **What changes are included?**
Changes on catalan.yaml file only.


### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? PARTIALLY
2026-03-05 09:28:25 -05:00
Zach Nelson
a826569a0f refactor: Avoid rebuilding cache path strings (#1300)
## Summary

**What is the goal of this PR?**

Avoid building cache path strings twice, once to check existence of the
file and a second time to delete the file.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-03-04 20:48:02 -05:00
jpirnay
88594077aa fix: Extend missing / amend existing German translations (#1226)
## Summary

* **What is the goal of this PR?** Extend missing / amend existing
German translations
* **What changes are included?** German.yaml

## Additional Context

---

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-03-03 11:02:04 -05:00
jpirnay
ce0b439aa3 feat: User setting for image display (#1291)
## Summary

**What is the goal of this PR?** Add a user setting to decide image
support: display, show placeholder instead, supress fully

Fixes #1289

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-03-03 09:59:06 -06:00
Uri Tauber
019587bb77 fix: add Technically Unsupported section to SCOPE.md (#1295)
## Summary

**Goal of this PR**
Add an **"In-scope — technically not supported"** section to `SCOPE.md`.

This clarifies hardware/UX limitations (e.g., clock support) and is
intended to reduce recurring feature requests on topics already
discussed.

Based on discussions in #287 and #626.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-03-03 09:57:57 -06:00
Dave Allie
4388bf8cc7 fix: Enable DESTRUCTOR_CLOSES_FILE flag (#1075)
## Summary

* Enable `DESTRUCTOR_CLOSES_FILE` flag
* We're never intending to not close files, so if we accidentally leave
them open as they're destructured, this will help close them.

## Additional Context

* As spotted in
https://github.com/crosspoint-reader/crosspoint-reader/pull/869, there
are cases where we were accidentally not closing files

Looks to use about 5K of flash.

```
RAM:   [===       ]  31.5% (used 103100 bytes from 327680 bytes)
Flash: [=======   ]  68.9% (used 4513220 bytes from 6553600 bytes)
```

```
RAM:   [===       ]  31.5% (used 103100 bytes from 327680 bytes)
Flash: [=======   ]  68.9% (used 4518498 bytes from 6553600 bytes)
```


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-03-03 08:41:02 -06:00
Mirus
6de8b7a666 chore: new Ukrainian localization strings (#1270)
## Summary

* **What changes are included?**
New Ukrainian localization strings

## Additional Context

auto turn functionality
https://github.com/crosspoint-reader/crosspoint-reader/pull/1219

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO **_
2026-03-03 00:01:47 -06:00
cottongin
c09f7b4a22 feat: add ".." go-up entry to directory picker
The folder picker had no visible way to navigate to a parent directory.
Add a ".." list item (shown when not at root) so users can go up by
selecting it, matching standard file-picker conventions.

Made-with: Cursor
2026-03-02 15:28:29 -05:00
cottongin
7eaced602f feat: add post-download prompt with open book / back to listing options
After an OPDS download completes, show a prompt screen instead of
immediately returning to the catalog. The user can choose to open the
book for reading or go back to the listing. A live countdown (5s, fast
refresh) auto-selects the configured default; any button press cancels
the timer. A per-server "After Download" setting controls the default
action (back to listing for backward compatibility).

Made-with: Cursor
2026-03-02 15:27:53 -05:00
cottongin
f955cf2fb4 feat: add OPDS server reordering with sortOrder field and numeric stepper
Servers are sorted by a persistent sortOrder field (ties broken
alphabetically). On-device editing uses a new NumericStepperActivity
with side buttons for ±1 and face buttons for ±10. The web UI gets
up/down arrow buttons and a POST /api/opds/reorder endpoint.

Made-with: Cursor
2026-03-02 14:35:36 -05:00
Xuan-Son Nguyen
307a6608f0 chore: remove rendundant xTaskCreate (#1264)
## Summary

Ref discussion:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1222#discussion_r2865402110

Important note that this is a bug-for-bug fix. In reality, this branch
`WiFi.status() == WL_CONNECTED` is pretty much a dead code because the
entry point of these 2 activities don't use wifi.

It is better to refactor the management of network though, but it's
better to be a dedicated PR.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
2026-03-02 13:30:55 +01:00
Dani Poveda
a350492571 fix: improve and add Spanish translations (#1254)
## Summary

* **What is the goal of this PR?** 
    - Improve and add the latest missing Spanish translations

* **What changes are included?**
- Add missing spaces and remove extra unneeded ones (spaces at the end
of certain strings and others, i.e. the one introduced in the string
`Smart Device`; actually, `SmartDevice` is the correct Calibre plugin
name)
    - Normalise the use of caps in certain strings
- Adapting the translation to the one found in related third-party
software (i.e. Spanish translation for the word `plugin` in Calibre is
`complemento`)
- Shortening some translations to make them smaller and fit better in
screen
- Rewording ambiguous translations (i.e. `Volver a inicio` could mean to
go back to Home, but also to go back to the first page of the current
book, so I changed it for a more specific action, `Volver al menú
Inicio`)

## Additional Context

* **Missing spaces caused a lack of clarity** 

    - My main motivation for this PR was the following:
        <details>

        <summary>Screenshots:</summary>
        
        In English:

![English.bmp](https://github.com/user-attachments/files/25650211/English.bmp)
        
        In Spanish:

![Spanish.bmp](https://github.com/user-attachments/files/25650225/Spanish.bmp)

        </details>

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-02 13:29:01 +01:00
jpirnay
ef02737c89 feat: Prefer ".sleep" over "sleep" for custom image directory (#948)
## Summary

* Custom sleep screen images now load from /.sleep directory
(preferred), falling back to /sleep for backwards compatibility. The
dot-prefix keeps the directory hidden from the file browser.
* Rewrote User Guide section 3.6 to document all six sleep screen modes,
cover settings, and the updated custom image setup.

## Additional Context

* The sleep directoy entry while browsing files was distracting.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _NO_
2026-03-02 13:28:14 +01:00
jpirnay
aff93f1dc0 fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary

* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:

* **What changes are included?**
  1. Negative text-indent was silently ignored

Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
      render the word far off-screen.

   2. extraParagraphSpacing suppressed hanging indents

Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
      and must always apply.

   3. em unit was calibrated against line height, not font size

emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).

## Additional Context

Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }

<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).

After: the bullet marker hangs at 2 em; continuation lines align at 3
em.

<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
cottongin
3628d8eb37 feat: port upstream KOReader sync PRs (#1185, #1217, #1090)
Port three unmerged upstream PRs with adaptations for the fork's
callback-based ActivityWithSubactivity architecture:

- PR #1185: Cache KOReader document hash using mtime fingerprint +
  file size validation to avoid repeated MD5 computation on sync.
- PR #1217: Proper KOReader XPath synchronisation via new
  ChapterXPathIndexer (Expat-based on-demand XHTML parsing) with
  XPath-first mapping and percentage fallback in ProgressMapper.
- PR #1090: Push Progress & Sleep menu option with PUSH_ONLY sync
  mode. Adapted to fork's callback pattern with deferFinish() for
  thread-safe completion. Modified to sleep silently on any failure
  (hash, upload, no credentials) rather than returning to reader.

Made-with: Cursor
2026-03-02 05:19:14 -05:00
cottongin
42011d5977 feat: add directory picker for OPDS downloads with per-server default path
When downloading a book via OPDS, a directory picker now lets the user
choose the save location instead of always saving to the SD root. Each
OPDS server has a configurable default download path (persisted in
opds.json) that the picker opens to. Falls back to "/" if the saved
path no longer exists on disk.

- Add DirectoryPickerActivity (browse-only directory view with "Save Here")
- Add PICKING_DIRECTORY state to OpdsBookBrowserActivity
- Add downloadPath field to OpdsServer with JSON serialization
- Add Download Path setting to OPDS server edit screen
- Extract sortFileList() to StringUtils for shared use
- Add i18n strings: STR_SAVE_HERE, STR_SELECT_FOLDER, STR_DOWNLOAD_PATH

Made-with: Cursor
2026-03-02 04:28:57 -05:00
cottongin
2aa13ea2de feat: port upstream OPDS improvements (PRs #1207, #1209)
Port two upstream PRs:

- PR #1207: Replace manual chunked download loop with
  HTTPClient::writeToStream via a FileWriteStream adapter, improving
  reliability for OPDS file downloads including chunked transfers.

- PR #1209: Add support for multiple OPDS servers with a new
  OpdsServerStore (JSON persistence with MAC-based password obfuscation),
  OpdsServerListActivity and OpdsSettingsActivity UIs, per-server
  credentials passed to HttpDownloader, web UI management endpoints,
  and migration from legacy single-server settings.

Made-with: Cursor
2026-02-26 19:14:59 -05:00
cottongin
19b6ad047b feat: add silent NTP time sync on boot via saved WiFi credentials
New "Auto Sync on Boot" toggle in Clock Settings. When enabled, a
background FreeRTOS task scans for saved WiFi networks at boot,
connects, syncs time via NTP, then tears down WiFi — all without
blocking boot or requiring user interaction. If no saved network is
found after two scan attempts (with a 3-second retry gap), it bails
silently.

Conflict guards (BootNtpSync::cancel()) added to all WiFi-using
activities so the background task cleans up before any user-initiated
WiFi flow. Also fixes clock not appearing in the header until a button
press by detecting the invalid→valid time transition after NTP sync.

Made-with: Cursor
2026-02-26 18:21:13 -05:00
cottongin
2eae521b6a feat: add BmpViewer activity for viewing .bmp images in file browser (port upstream PR #887)
New BmpViewerActivity opens, parses, and renders BMP files with centered
aspect-ratio-preserving display and localized back navigation. Library
file filter extended to include .bmp. ReaderActivity routes BMP paths to
the new viewer. LyraTheme button hint backgrounds switched to rounded
rect fills to prevent overflow artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 18:37:43 -05:00
cottongin
9d9bc019a2 docs: rewrite README for mod branch, bump version to 1.1.2
Rewrite README.md to distinguish the mod fork from upstream CrossPoint
Reader. Add mod feature documentation (bookmarks, dictionary, clock, book
management, table rendering, reader menu overhaul, etc.), updated feature
checklist, upstream compatibility notes, and mod-specific build instructions.

Bump version from 1.1.1-rc to 1.1.2 to align with upstream approaching
release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 18:08:09 -05:00
cottongin
ff33b2b3be fix: correct hyphenation of URLs (port upstream PR #1068)
Add '/' as explicit hyphen delimiter and relax the alphabetic-surround
requirement for '/' and '-' in buildExplicitBreakInfos so URL path
segments can be line-wrapped. Includes repeated-separator guard to
prevent breaks between consecutive identical separators (e.g. "http://").

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 17:26:09 -05:00
cottongin
0e2440aea8 fix: resolve end-of-book deadlock, long-press guards, archive UX, and home screen refresh
- Fix device freeze at end-of-book by deferring EndOfBookMenuActivity
  creation from render() to loop() (avoids RenderLock deadlock) in
  EpubReaderActivity and XtcReaderActivity
- Add initialSkipRelease to BookManageMenuActivity to prevent stale
  Confirm release from triggering actions when opened via long-press
- Add initialSkipRelease to MyLibraryActivity for long-press Browse
  Files -> archive navigation
- Thread skip-release through HomeActivity callback and main.cpp
- Fix HomeActivity stale cover buffer after archive/delete by fully
  resetting render state (freeCoverBuffer, firstRenderDone, etc.)
- Swap short/long-press actions in .archive context: short-press opens
  manage menu, long-press unarchives and opens the book
- Add deferred open pattern (pendingOpenPath) to wait for Confirm
  release before navigating to reader after unarchive
- Add BookManager::cleanupEmptyArchiveDirs() to remove empty parent
  directories after unarchive/delete inside .archive
- Add optional unarchivedPath output parameter to BookManager::unarchiveBook
- Restyle EndOfBookMenuActivity to standard list layout with proper
  header, margins, and button hints matching other screens
- Change EndOfBookMenuActivity back button hint to "« Back"
- Add Table of Contents option to EndOfBookMenuActivity

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 07:37:36 -05:00
cottongin
39ef1e6d78 fix: remove invalid RenderLock::unlock() call in end-of-book handler
RenderLock is a pure RAII wrapper with no unlock() method. The lock
releases naturally when render() returns. Build now succeeds cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 03:16:24 -05:00
cottongin
3cc127d658 fix: ClearCacheActivity now clears txt_* caches and recents list
Add txt_* prefix to the directory check so TXT book caches are also
removed. After clearing all cache directories, call RECENT_BOOKS.clear()
to remove stale entries that would show missing covers on the home screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 03:05:19 -05:00
cottongin
98146f2545 feat: add EndOfBookMenuActivity replacing static end-of-book text
Interactive menu shown when reaching the end of a book with options:
Archive Book, Delete Book, Back to Beginning, Close Book, Close Menu.
Wired into EpubReaderActivity, XtcReaderActivity, and TxtReaderActivity
(TXT shows menu when user tries to advance past the last page).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 03:04:48 -05:00
cottongin
f5b708424d feat: replace Delete Book Cache with Manage Book in reader menu
EpubReaderMenuActivity now shows "Manage Book" instead of "Delete
Book Cache". Selecting it opens BookManageMenuActivity as a sub-activity
with Archive, Delete, Delete Cache, and Reindex options. New menu
actions (ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK, REINDEX_BOOK_FULL)
are forwarded to EpubReaderActivity and handled via BookManager.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 03:02:30 -05:00
cottongin
1c19899aa3 feat: add long-press on HomeActivity for book management and archive browsing
Long-press Confirm on a recent book opens the BookManageMenuActivity.
Long-press Confirm on Browse Files navigates directly to /.archive/.
Wires onMyLibraryOpenWithPath callback through main.cpp to HomeActivity.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:59:35 -05:00
cottongin
390f10f30d feat: add long-press Confirm for book management in file browser and recents
Long-pressing Confirm on a book file in MyLibraryActivity or
RecentBooksActivity opens the BookManageMenuActivity popup with
Archive/Delete/Delete Cache/Reindex options. Actions are executed
via BookManager and the file list is refreshed afterward.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:57:19 -05:00
cottongin
49471e36f1 refactor: change browse activities to ActivityWithSubactivity
Change HomeActivity, MyLibraryActivity, and RecentBooksActivity base
class from Activity to ActivityWithSubactivity. Adds subActivity
guard at top of each loop(). No new behavior, just enabling sub-activity
hosting for the upcoming BookManageMenuActivity integration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:55:29 -05:00
cottongin
c44ac0272a feat: add BookManageMenuActivity popup sub-activity
Contextual popup menu for book management with Archive/Unarchive,
Delete, Delete Cache Only, and Reindex options. Supports long-press
on Reindex to trigger full reindex including cover/thumbnail regen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:54:14 -05:00
cottongin
29954a3683 feat: add BookManager utility and RecentBooksStore::clear()
BookManager provides static functions for archive/unarchive/delete/
deleteCache/reindex operations on books, centralizing cache path
computation and file operations. Archive preserves directory structure
under /.archive/ and renames cache dirs to match new path hashes.

RecentBooksStore: :clear() added for bulk cache clearing use case.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:52:38 -05:00
cottongin
3eddb07a1a feat(i18n): add string keys for book management feature
Add STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK,
STR_DELETE_BOOK, STR_DELETE_CACHE_ONLY, STR_REINDEX_BOOK,
STR_BROWSE_ARCHIVE, status messages, STR_BACK_TO_BEGINNING,
and STR_CLOSE_MENU for the manage books and end-of-book menus.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:51:14 -05:00
cottongin
f443f5dde0 feat(hal): expose rename() on HalStorage
Forward SDCardManager::rename() through the HAL layer for
file/directory move operations needed by book archiving.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:50:27 -05:00
cottongin
3d51dfeeb7 feat: Add NTP clock sync to Clock settings
Adds a "Sync Clock" action in Settings > Clock that connects to WiFi
(auto-connecting to saved networks or prompting for selection) and
performs a blocking NTP time sync. Shows the synced time on success
with an auto-dismiss countdown, or an error on failure.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 02:19:38 -05:00
cottongin
4dadea1a03 perf: Port upstream PR #1027 — word-width cache and hyphenation early exit
Reduces ParsedText::layoutAndExtractLines CPU time 5–9% via two
independent optimizations from jpirnay's PR #1027:

- 128-entry direct-mapped word-width cache (4 KB BSS, FNV-1a hash)
  absorbs redundant getTextAdvanceX calls across paragraphs
- Early exit in hyphenateWordAtIndex when prefix exceeds available
  width (ascending byte-offset order guarantees monotonic widths)
- Reusable prefix string buffer eliminates per-candidate substr allocs
- Reserve hint for lineBreakIndices in computeLineBreaks

List-specific upstream changes (splice, iterator style) not applicable
as mod already uses std::vector (PR #1038). Benchmark infrastructure
excluded (removed by author in final commit).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 01:48:58 -05:00
cottongin
0d9a1f4f89 perf: Port upstream PR #1055 — byte-level framebuffer writes
Replace per-pixel drawPixel calls with byte-level framebuffer writes
for fillRect, axis-aligned drawLine, and fillRectDither. Adds
fillPhysicalHSpanByte/fillPhysicalHSpan helpers that write directly
to physical rows with memset and partial-byte masking.

Also applies coderabbit nitpick: fillPolygon scanline fill now uses
fillPhysicalHSpan for Landscape orientations.

Upstream: https://github.com/crosspoint-reader/crosspoint-reader/pull/1055
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 01:14:30 -05:00
cottongin
1b350656a5 fix: Restore normal CPU frequency before drawing sleep screen
When auto-sleep triggers after inactivity, the CPU remains at 10 MHz
(low power mode) during sleep screen rendering, causing it to draw
much slower than expected.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 20:28:57 -05:00
cottongin
51dc498768 feat: Expandable selected row for long filenames in File Browser
When the selected row's filename overflows the available text width
(with extension), the row expands to 2 lines with smart text wrapping.
The file extension moves to the second row (right-aligned). Non-selected
rows retain single-line truncation.

Key behaviors:
- 3-tier text wrapping: preferred delimiters (" - ", " -- ", en/em-dash),
  word boundaries, then character-level fallback
- Row-height line spacing for natural visual rhythm
- Icons aligned with line 1 (LyraTheme)
- Pagination uses effectivePageItems with anti-leak clamping to prevent
  page boundary shifts while ensuring all items remain accessible
- Boundary item duplication: items bumped from a page due to expansion
  appear at the top of the next page, guarded against cascading

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 19:42:56 -05:00
cottongin
406c3aeace fix: Port upstream PRs #1038, #1037, #1045, #1019
- #1038 (partial): Add .erase() for consumed words in layoutAndExtractLines
  to fix redundant early flush bug; fix wordContinues flag in hyphenateWordAtIndex
- #1037: Add combining mark handling for hyphenation (NFC-like precomposition)
  and rendering (base glyph tracking in EpdFont, GfxRenderer including CCW)
- #1045: Shorten STR_FORGET_BUTTON labels across all 9 translation files
- #1019: Display file extensions in File Browser via getFileExtension helper
- Pull romanian.yaml from upstream/master (merged PR #987)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 16:27:59 -05:00
cottongin
55a1fef01a fix: Port upstream 1.1.0-rc PRs #1014, #1018, #990 and align #1002
Port three new upstream commits and align the existing #1002 port:

- PR #1014: Strip unused CSS rules by filtering unsupported selector
  types (+, >, [, :, #, ~, *, descendants) in processRuleBlockWithStyle.
  Fix normalized() trailing whitespace to also strip newlines.
- PR #1018: Add deleteCache() to CssParser, move CSS_CACHE_VERSION to
  static class member, remove stale cache on version mismatch, invalidate
  section caches (Storage.removeDir) when CSS is rebuilt. Refactor
  parseCssFiles() to early-return when cache exists.
- PR #990: Adapt classic theme continue-reading card width to cover
  aspect ratio (clamped to 90% screen width), increase homeTopPadding
  20->40, fix centering with rect.x offset for boxX/continueBoxX.
- #1002 alignment: Add tryInterpretLength() to skip non-numeric CSS
  values (auto, inherit), add "both width and height set" image sizing
  branch in ChapterHtmlSlimParser.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 15:52:30 -05:00
cottongin
18be265a4a fix: Re-apply upstream PRs #1005, #1010, #1003
Re-applies changes that were accidentally discarded during a prior
dry-run cherry-pick reset (git checkout -- .).

- PR #1005: Use HalPowerManager for battery percentage (uint16_t return
  type, remove Battery.h, update theme files)
- PR #1010: Fix dangling pointer in onGoToReader()
- PR #1003: Render image placeholders while waiting for decode (adds
  isCached, renderPlaceholder, renderTextOnly, countUncachedImages,
  renderImagePlaceholders)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 22:31:07 -05:00
cottongin
3a0641889f perf: Port upstream font drawing performance optimization (PR #978)
Cherry-pick upstream commit 07d715e which refactors renderChar and
drawTextRotated90CW into a template-based renderCharImpl, hoisting
the is2Bit branch outside inner pixel loops for 15-23% speedup.

Additionally extends the template with Rotated90CCW to fix two bugs
in the mod's drawTextRotated90CCW: operator precedence in bmpVal
calculation and missing compressed font support via getGlyphBitmap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 22:20:44 -05:00
cottongin
ad282cadfe fix: Align double FAST_REFRESH image rendering with upstream PR #957
Reorder refresh branches so image+AA pages always use the double
FAST_REFRESH technique instead of occasionally falling through to
HALF_REFRESH when the refresh counter expires. Image pages no longer
count toward the full refresh cadence. Remove experimental Method B
toggle (USE_IMAGE_DOUBLE_FAST_REFRESH / displayWindow).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:30:10 -05:00
cottongin
c8ba4fe973 fix: Port upstream CSS-aware image sizing (PR #1002)
Parse CSS height/width into CssStyle for images and use aspect-ratio-
preserving logic when CSS dimensions are set. Falls back to viewport-fit
scaling when no CSS dimensions are present. Includes divide-by-zero
guards and viewport clamping with aspect ratio rescaling.

- Add imageHeight field to CssStyle/CssPropertyFlags
- Parse CSS height declarations into imageHeight
- Add imageHeight + width to cache serialization (bump cache v2->v3)
- Replace viewport-fit-only image scaling with CSS-aware sizing

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:21:31 -05:00
cottongin
c1b8e53138 fix: Port upstream 1.1.0-rc fixes (glyph null-safety, PNGdec wide image buffer)
Cherry-pick two bug fixes from upstream PR #992:

- fix(GfxRenderer): Null-safety in getSpaceWidth/getTextAdvanceX to
  prevent Load access fault when bold/italic font variants lack certain
  glyphs (upstream 3e2c518)
- fix(PNGdec): Increase PNG_MAX_BUFFERED_PIXELS to 16416 for 2048px
  wide images and add pre-decode buffer overflow guard (upstream b8e743e)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 13:20:30 -05:00
cottongin
0fda9031fd fix: Use double FAST_REFRESH for dithered letterbox sleep covers
Replace HALF_REFRESH with double FAST_REFRESH technique for the BW
pass when dithered letterbox fill is active. This avoids the e-ink
crosstalk and image corruption that occurred when HALF_REFRESH drove
large areas of dithered gray pixels simultaneously.

Revert the hash-based block dithering workaround (bayerCrossesBwBoundary,
hashBlockDither) back to standard Bayer dithering for all gray ranges,
since the root cause was HALF_REFRESH rather than the dithering pattern
itself.

Letterbox fill is now included in all three render passes (BW, LSB, MSB)
so the greyscale LUT treats letterbox pixels identically to cover pixels,
maintaining color-matched edges.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 11:33:45 -05:00
cottongin
013a738144 chore: post-sync cleanup and clang-format
- Remove stale Lyra3CoversTheme.h (functionality merged into LyraTheme)
- Fix UITheme.cpp to use LyraTheme for LYRA_3_COVERS theme variant
- Update open-x4-sdk submodule to 91e7e2b (drawImageTransparent support)
- Run clang-format on all source files

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 10:46:25 -05:00
Adrian Wilkins-Caruana
c1d1e98909 perf: Reduce overall flash usage by 30.7% by compressing built-in fonts (#831)
**What is the goal of this PR?**

Compress reader font bitmaps to reduce flash usage by 30.7%.

**What changes are included?**

- New `EpdFontGroup` struct and extended `EpdFontData` with
`groups`/`groupCount` fields
- `--compress` flag in `fontconvert.py`: groups glyphs (ASCII base group
+ groups of 8) and compresses each with raw DEFLATE
- `FontDecompressor` class with 4-slot LRU cache for on-demand
decompression during rendering
- `GfxRenderer` transparently routes bitmap access through
`getGlyphBitmap()` (compressed or direct flash)
- Uses `uzlib` for decompression with minimal heap overhead.
- 48 reader fonts (Bookerly, NotoSans 12-18pt, OpenDyslexic) regenerated
with compression; 5 UI fonts unchanged
- Round-trip verification script (`verify_compression.py`) runs as part
of font generation

| | baseline | font-compression | Difference |
|--|--------|-----------------|------------|
| Flash (ELF) | 6,302,476 B (96.2%) | 4,365,022 B (66.6%) | -1,937,454 B
(-30.7%) |
| firmware.bin | 6,468,192 B | 4,531,008 B | -1,937,184 B (-29.9%) |
| RAM | 101,700 B (31.0%) | 103,076 B (31.5%) | +1,376 B (+0.5%) |

Comparison of uncompressed baseline vs script-based group compression
(4-slot LRU cache, cleared each page). Glyphs are grouped by Unicode
block (ASCII, Latin-1, Latin Extended-A, Combining Marks, Cyrillic,
General Punctuation, etc.) instead of sequential groups of 8.

| | Baseline | Compressed (cold cache) | Difference |
|---|---|---|---|
| **Median** | 414.9 ms | 431.6 ms | +16.7 ms (+4.0%) |
| **Pages** | 37 | 37 | |

| | Baseline | Compressed (cold cache) | Difference |
|---|---|---|---|
| **Heap free (median)** | 187.0 KB | 176.3 KB | -10.7 KB |
| **Heap free (min)** | 186.0 KB | 166.5 KB | -19.5 KB |
| **Largest block (median)** | 148.0 KB | 128.0 KB | -20.0 KB |
| **Largest block (min)** | 148.0 KB | 120.0 KB | -28.0 KB |

| | Misses/page | Hit rate |
|---|---|---|
| **Compressed (cold cache)** | 2.1 | 99.85% |

------

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

  Did you use AI tools to help write this code? _**YES**_
Implementation was done by Claude Code (Opus 4.6) based on a plan
developed collaboratively. All generated font headers were verified with
an automated round-trip decompression test. The firmware was compiled
successfully but has not yet been tested on-device.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 10:39:11 -05:00
Егор Мартынов
6403ce6309 fix: go to prev page on the first one, get teleported to the end of book (#970)
## Summary

1. Go to the first page in a .epub file.
2. Hit `Up` button
3. Get teleported to the last page :)

`TxtRenderActivity` seems to have this if check, but EPUB one does not.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:36:57 -05:00
Sam Lord
109f95df78 feat: Scale cover images up if they're smaller than the device resolution (#964)
## Summary

**What is the goal of this PR?**
* Implement feature request
[#954](https://github.com/crosspoint-reader/crosspoint-reader/issues/954)
* Ensure cover images are scaled up to match the dimensions of the
screen, as well as scaled down

**What changes are included?**
* Naïve implementation for scaling up the source image

## Additional Context

If you find the extra comments to be excessive I can pare them back. 

Edit: Fixed title

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
2026-02-19 10:36:51 -05:00
Zach Nelson
de981f5072 fix: Correct word width and space calculations (#963)
## Summary

**What is the goal of this PR?**

This change fixes an issue I noticed while reading where occasionally,
especially in italics, some words would have too much space between
them. The problem was that word width calculations were including any
negative X overhang, and combined with a space before the word, that can
lead to an inconsistently large space.

## Additional Context

Screenshots of some problematic text:

| In CrossPoint 1.0 | With this change |
| -- | -- |
| <img
src="https://github.com/user-attachments/assets/87bf0e4b-341f-4ba9-b3ea-38c13bd26363"
width="400" /> | <img
src="https://github.com/user-attachments/assets/bf11ba20-c297-4ce1-aa07-43477ef86fc2"
width="400" /> |

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:36:44 -05:00
Sam Lord
2bcc1c1495 fix: Skip large CSS files to prevent crashes (#952)
## Summary

**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* Fixes:
https://github.com/crosspoint-reader/crosspoint-reader/issues/947

**What changes are included?**
* Check to see if there's free heap memory before processing CSS (should
we be doing this type of check or is it better to just crash if we
exhaust the memory?)
* Skip CSS files larger than 128kb

## Additional Context

* I found that a copy of `Release it` contained a 250kb+ CSS file, from
the homepage of the publisher. It has nothing to do with the epub, so we
should just skip it
* Major question: Are there better ways to detect CSS that doesn't
belong in a book, or is this size-based approach valid?
* Another question: Are there any epubs we know of that legitimately
include >128kb CSS files?

Code changes themselves created with an agent, all investigation and
write-up done by human. If you (the maintainers) would prefer a
different fix for this issue, let me know.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
2026-02-19 10:36:38 -05:00
jpirnay
aa7c0a882a feat: Add 4bit bmp support (#944)
## Summary

* What is the goal of this PR?
- Allow users to create custom sleep screen images with standard tools
(ImageMagick, GIMP, etc.) that render cleanly on the e-ink display
without dithering artifacts. Previously, avoiding dithering required
non-standard 2-bit BMPs that no standard image editor can produce. ( see
issue #931 )

* What changes are included?
- Add 4-bit BMP format support to Bitmap.cpp (standard format, widely
supported by image tools)
- Auto-detect "native palette" images: if a BMP has ≤4 palette entries
and all luminances map within ±21 of the display's native gray levels
(0, 85, 170, 255), skip dithering entirely and direct-map pixels
- Clarify pixel processing strategy with three distinct paths:
error-diffusion dithering, simple quantization, or direct mapping
- Add scripts/generate_test_bmps.py for generating test images across
all supported BMP formats

## Additional Context

* The e-ink display has 4 native gray levels. When a BMP already uses
exactly those levels, dithering adds noise to what should be clean
output. The native palette detection uses a ±21 tolerance (~10%) to
handle slight rounding from color space conversions in image tools.
Users can now create a 4-color grayscale BMP with (imagemagic example):
```
convert input.png -colorspace Gray -colors 4 -depth 
```
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** YES**_
2026-02-19 10:36:32 -05:00
Zach Nelson
950faf4cd2 perf: Avoid redundant font map lookups (#933)
## Summary

**What is the goal of this PR?**

Several methods in GfxRenderer were doing a `count()` followed by `at()`
on the fonts map, effectively doing the same map lookup unnecessarily.
This can be avoided by doing a single `find()` and reusing the iterator.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:36:27 -05:00
jpirnay
e5d574a07a fix: add bresenham for arbitrary lines (#923)
## Summary

* GfxRender did handle horizontal and vertical lines but had a TODO for
arbitrary lines.
* Added integer based Bresenham line drawing 
  
## Additional Context

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:36:21 -05:00
jpirnay
c8ddb6b61d fix: Fix kosync repositioning issue (#783)
## Summary

* Original implementation had inconsistent positioning logic:
- When XPath parsing succeeded: incorrectly set pageNumber = 0 (always
beginning of chapter)
- When XPath parsing failed: used percentage for positioning (worked
correctly)
- Result: Positions restored to wrong locations depending on XPath
parsing success
  - Mentioned in Issue #581 
* Solution
- Unified ProgressMapper::toCrossPoint() to use percentage-based
positioning exclusively for both spine identification and intra-chapter
page calculation, eliminating unreliable XPath parsing entirely.

## Additional Context

* ProgressMapper.cpp: Simplified toCrossPoint() to always use percentage
for positioning, removed parseDocFragmentIndex() function
* ProgressMapper.h: Updated comments and removed unused function
declaration
* Tests confirmed appropriate positioning
* __Notabene: the syncing to another device will (most probably) end up
at the current chapter of crosspoints reading position. There is not
much we can do about it, as KOReader needs to have the correct XPath
information - we can only provide an apporximate position (plus
percentage) - the percentage information is not used in KOReaders
current implementation__
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? YES
2026-02-19 10:36:15 -05:00
Zach Nelson
ef52af1a52 fix: Added missing up/down button labels (#935)
**What is the goal of this PR?**

In some places, button labels are omitted intentionally because the
button has no purpose in the activity. I noticed a few obvious cases,
like Home > File Transfer and Settings > System > Language, where the up
and down button labels were missing. This change fixes those and all
similar instances I could find.

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:36:03 -05:00
Uri Tauber
8a28755c69 fix: Update Translators list (#927)
## Summary

* **What is the goal of this PR?** Update translators.md to include all
the contributors from #728

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-19 10:35:18 -05:00
ariel-lindemann
4b713f40f1 feat: increase keyboard font size for classic theme (#897)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Adresses Feature Request #896 

* **What changes are included?**

Changed key dimensions, initial positions and margins.

## Additional Context

The keyboard now looks like this:

![image](https://github.com/user-attachments/assets/e2b8f3fe-e54a-4a44-9a29-2ef9f2c8dffb)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:35:12 -05:00
Xuan-Son Nguyen
ab5e18aca3 feat: add script for listing objects in flash (#880)
## Summary

Adding a simple script to list objects and its size. I know `pio`
already had the "analyze" function but it never works in my case.

Ref discussion:
https://github.com/crosspoint-reader/crosspoint-reader/discussions/862

To use it:

```sh
scripts/script_profile_mem.sh
```

Example:

```
============================================
Top 10 largest symbols in section: .dram0.bss
Total section size: 85976 bytes (83.96 KB)
============================================
    0000bb98 (  46.90 KB)  display
    00000ed8 (   3.71 KB)  g_cnxMgr
    00000ad8 (   2.71 KB)  ftm_initiator
    00000830 (   2.05 KB)  xIsrStack
    000005b4 (   1.43 KB)  packet.8427
    000004e0 (   1.22 KB)  _ZN12_GLOBAL__N_17ctype_wE
    000004a0 (   1.16 KB)  dns_table
    0000049c (   1.15 KB)  s_wifi_nvs
    00000464 (   1.10 KB)  s_coredump_stack
    0000034c (   0.82 KB)  gWpaSm

============================================
Top 10 largest symbols in section: .dram0.data
Total section size: 13037 bytes (12.73 KB)
============================================
    000003c0 (   0.94 KB)  TxRxCxt
    00000328 (   0.79 KB)  phy_param
    000001d0 (   0.45 KB)  g_wifi_osi_funcs
    0000011b (   0.28 KB)  _ZN18CrossPointSettings8instanceE
    000000e6 (   0.22 KB)  country_info_24ghz
    000000dc (   0.21 KB)  g_eb_list_desc
    000000c0 (   0.19 KB)  s_fd_table
    000000b8 (   0.18 KB)  g_timer_info
    000000a8 (   0.16 KB)  rc11NSchedTbl
    000000a0 (   0.16 KB)  g_rmt_objects

============================================
Top 200 largest symbols in section: .flash.rodata
Total section size: 4564375 bytes (4457.40 KB)
============================================
    000325b7 ( 201.43 KB)  _ZL12de_trie_data
    0001cbd8 ( 114.96 KB)  _ZL29notosans_18_bolditalicBitmaps
    0001ca38 ( 114.55 KB)  _ZL29bookerly_18_bolditalicBitmaps
    0001bd3f ( 111.31 KB)  _ZL23bookerly_18_boldBitmaps
    0001af54 ( 107.83 KB)  _ZL23notosans_18_boldBitmaps
    0001abcc ( 106.95 KB)  _ZL25bookerly_18_italicBitmaps
    0001a341 ( 104.81 KB)  _ZL25notosans_18_italicBitmaps
    0001a0a5 ( 104.16 KB)  _ZL26bookerly_18_regularBitmaps
    0001890c (  98.26 KB)  _ZL26notosans_18_regularBitmaps
    000188cc (  98.20 KB)  _ZL33opendyslexic_14_bolditalicBitmaps
    000170ca (  92.20 KB)  _ZL29notosans_16_bolditalicBitmaps
    00015e7f (  87.62 KB)  _ZL29bookerly_16_bolditalicBitmaps
    00015acb (  86.70 KB)  _ZL23notosans_16_boldBitmaps
    00015140 (  84.31 KB)  _ZL23bookerly_16_boldBitmaps
    00014f0c (  83.76 KB)  _ZL25notosans_16_italicBitmaps
    00014d54 (  83.33 KB)  _ZL29opendyslexic_14_italicBitmaps
    00014766 (  81.85 KB)  _ZL27opendyslexic_14_boldBitmaps
    0001467f (  81.62 KB)  _ZL25bookerly_16_italicBitmaps
    00013c46 (  79.07 KB)  _ZL26bookerly_16_regularBitmaps
    00013934 (  78.30 KB)  _ZL26notosans_16_regularBitmaps
    00012572 (  73.36 KB)  _ZL33opendyslexic_12_bolditalicBitmaps
    00011cee (  71.23 KB)  _ZL29notosans_14_bolditalicBitmaps
    00011547 (  69.32 KB)  _ZL30opendyslexic_14_regularBitmaps
    0001153c (  69.31 KB)  _ZL29bookerly_14_bolditalicBitmaps
    00010c2e (  67.04 KB)  _ZL23notosans_14_boldBitmaps
    0001096e (  66.36 KB)  _ZL23bookerly_14_boldBitmaps
    000103d2 (  64.96 KB)  _ZL25notosans_14_italicBitmaps
    000100d5 (  64.21 KB)  _ZL25bookerly_14_italicBitmaps
    0000f83e (  62.06 KB)  _ZL26bookerly_14_regularBitmaps
    0000f7fc (  62.00 KB)  _ZL29opendyslexic_12_italicBitmaps
    0000f42b (  61.04 KB)  _ZL26notosans_14_regularBitmaps
    0000f229 (  60.54 KB)  _ZL27opendyslexic_12_boldBitmaps
    0000d301 (  52.75 KB)  _ZL29notosans_12_bolditalicBitmaps
    0000d22f (  52.55 KB)  _ZL30opendyslexic_12_regularBitmaps
    0000cff4 (  51.99 KB)  _ZL29bookerly_12_bolditalicBitmaps
    0000cd12 (  51.27 KB)  _ZL33opendyslexic_10_bolditalicBitmaps
    0000cad2 (  50.71 KB)  _ZL23bookerly_12_boldBitmaps
    0000c60a (  49.51 KB)  _ZL23notosans_12_boldBitmaps
    0000c147 (  48.32 KB)  _ZL25bookerly_12_italicBitmaps
    0000c120 (  48.28 KB)  _ZL25notosans_12_italicBitmaps
    0000ba7e (  46.62 KB)  _ZL26bookerly_12_regularBitmaps
    0000b454 (  45.08 KB)  _ZL26notosans_12_regularBitmaps
    0000b1e0 (  44.47 KB)  _ZL29opendyslexic_10_italicBitmaps
    0000a939 (  42.31 KB)  _ZL27opendyslexic_10_boldBitmaps
    0000a0dd (  40.22 KB)  _ZL13FilesPageHtml
    0000949d (  37.15 KB)  _ZL30opendyslexic_10_regularBitmaps
    00008415 (  33.02 KB)  _ZL32opendyslexic_8_bolditalicBitmaps
    00008240 (  32.56 KB)  _ZL15ru_ru_trie_data
    00007587 (  29.38 KB)  _ZL28opendyslexic_8_italicBitmaps
    00006f4d (  27.83 KB)  _ZL26opendyslexic_8_boldBitmaps
    00006943 (  26.32 KB)  _ZL15en_us_trie_data
    00006196 (  24.40 KB)  _ZL29opendyslexic_8_regularBitmaps
    00004990 (  18.39 KB)  _ZL21ubuntu_12_boldBitmaps
    000042ce (  16.70 KB)  _ZL24ubuntu_12_regularBitmaps
    000036d0 (  13.70 KB)  _ZL25notosans_18_regularGlyphs
    000036d0 (  13.70 KB)  _ZL25notosans_16_regularGlyphs
    000036d0 (  13.70 KB)  _ZL25notosans_14_regularGlyphs
    000036d0 (  13.70 KB)  _ZL25notosans_12_regularGlyphs
    000036d0 (  13.70 KB)  _ZL24notosans_8_regularGlyphs
    000036d0 (  13.70 KB)  _ZL22notosans_18_boldGlyphs
    000036d0 (  13.70 KB)  _ZL22notosans_16_boldGlyphs
    000036d0 (  13.70 KB)  _ZL22notosans_14_boldGlyphs
    000036d0 (  13.70 KB)  _ZL22notosans_12_boldGlyphs
    000036c0 (  13.69 KB)  _ZL28notosans_18_bolditalicGlyphs
    000036c0 (  13.69 KB)  _ZL28notosans_16_bolditalicGlyphs
    000036c0 (  13.69 KB)  _ZL28notosans_14_bolditalicGlyphs
    000036c0 (  13.69 KB)  _ZL28notosans_12_bolditalicGlyphs
    000036c0 (  13.69 KB)  _ZL24notosans_18_italicGlyphs
    000036c0 (  13.69 KB)  _ZL24notosans_16_italicGlyphs
    000036c0 (  13.69 KB)  _ZL24notosans_14_italicGlyphs
    000036c0 (  13.69 KB)  _ZL24notosans_12_italicGlyphs
    00003627 (  13.54 KB)  _ZL21ubuntu_10_boldBitmaps
    00003551 (  13.33 KB)  _ZL12es_trie_data
    000030c4 (  12.19 KB)  _ZL24ubuntu_10_regularBitmaps
    00002eb0 (  11.67 KB)  _ZL28bookerly_18_bolditalicGlyphs
    00002eb0 (  11.67 KB)  _ZL28bookerly_16_bolditalicGlyphs
    00002eb0 (  11.67 KB)  _ZL28bookerly_14_bolditalicGlyphs
    00002eb0 (  11.67 KB)  _ZL28bookerly_12_bolditalicGlyphs
    00002eb0 (  11.67 KB)  _ZL25bookerly_18_regularGlyphs
    00002eb0 (  11.67 KB)  _ZL25bookerly_16_regularGlyphs
    00002eb0 (  11.67 KB)  _ZL25bookerly_14_regularGlyphs
    00002eb0 (  11.67 KB)  _ZL25bookerly_12_regularGlyphs
    00002eb0 (  11.67 KB)  _ZL24bookerly_18_italicGlyphs
    00002eb0 (  11.67 KB)  _ZL24bookerly_16_italicGlyphs
    00002eb0 (  11.67 KB)  _ZL24bookerly_14_italicGlyphs
    00002eb0 (  11.67 KB)  _ZL24bookerly_12_italicGlyphs
    00002eb0 (  11.67 KB)  _ZL22bookerly_18_boldGlyphs
    00002eb0 (  11.67 KB)  _ZL22bookerly_16_boldGlyphs
    00002eb0 (  11.67 KB)  _ZL22bookerly_14_boldGlyphs
    00002eb0 (  11.67 KB)  _ZL22bookerly_12_boldGlyphs
    00002d50 (  11.33 KB)  _ZL32opendyslexic_14_bolditalicGlyphs
    00002d50 (  11.33 KB)  _ZL32opendyslexic_12_bolditalicGlyphs
    00002d50 (  11.33 KB)  _ZL32opendyslexic_10_bolditalicGlyphs
    00002d50 (  11.33 KB)  _ZL31opendyslexic_8_bolditalicGlyphs
    00002d50 (  11.33 KB)  _ZL29opendyslexic_14_regularGlyphs
    00002d50 (  11.33 KB)  _ZL29opendyslexic_12_regularGlyphs
    00002d50 (  11.33 KB)  _ZL29opendyslexic_10_regularGlyphs
    00002d50 (  11.33 KB)  _ZL28opendyslexic_8_regularGlyphs
    00002d50 (  11.33 KB)  _ZL28opendyslexic_14_italicGlyphs
    00002d50 (  11.33 KB)  _ZL28opendyslexic_12_italicGlyphs
    00002d50 (  11.33 KB)  _ZL28opendyslexic_10_italicGlyphs
    00002d50 (  11.33 KB)  _ZL27opendyslexic_8_italicGlyphs
    00002d50 (  11.33 KB)  _ZL26opendyslexic_14_boldGlyphs
    00002d50 (  11.33 KB)  _ZL26opendyslexic_12_boldGlyphs
    00002d50 (  11.33 KB)  _ZL26opendyslexic_10_boldGlyphs
    00002d50 (  11.33 KB)  _ZL25opendyslexic_8_boldGlyphs
    00002bca (  10.95 KB)  _ZL25notosans_8_regularBitmaps
    0000294c (  10.32 KB)  _ZL16SettingsPageHtml
    000024f0 (   9.23 KB)  _ZL23ubuntu_12_regularGlyphs
    000024f0 (   9.23 KB)  _ZL23ubuntu_10_regularGlyphs
    000024f0 (   9.23 KB)  _ZL20ubuntu_12_boldGlyphs
    000024f0 (   9.23 KB)  _ZL20ubuntu_10_boldGlyphs
    00001b4c (   6.82 KB)  _ZL12fr_trie_data
    000012e8 (   4.73 KB)  ciphersuite_definitions
    00000c8d (   3.14 KB)  _ZL12HomePageHtml
    00000708 (   1.76 KB)  _ZL7Logo120
    00000708 (   1.76 KB)  _ZL7Logo120
    00000688 (   1.63 KB)  esp_err_msg_table
    00000613 (   1.52 KB)  _ZL12it_trie_data
    00000500 (   1.25 KB)  namingBitmap
    00000450 (   1.08 KB)  _ZN4mime9mimeTableE
    00000404 (   1.00 KB)  _ZNSt8__detail12__prime_listE
    00000340 (   0.81 KB)  ciphersuite_preference
    00000300 (   0.75 KB)  _ZL31bookerly_18_bolditalicIntervals
    00000300 (   0.75 KB)  _ZL31bookerly_16_bolditalicIntervals
    00000300 (   0.75 KB)  _ZL31bookerly_14_bolditalicIntervals
    00000300 (   0.75 KB)  _ZL31bookerly_12_bolditalicIntervals
    00000300 (   0.75 KB)  _ZL28bookerly_18_regularIntervals
    00000300 (   0.75 KB)  _ZL28bookerly_16_regularIntervals
    00000300 (   0.75 KB)  _ZL28bookerly_14_regularIntervals
    00000300 (   0.75 KB)  _ZL28bookerly_12_regularIntervals
    00000300 (   0.75 KB)  _ZL27bookerly_18_italicIntervals
    00000300 (   0.75 KB)  _ZL27bookerly_16_italicIntervals
    00000300 (   0.75 KB)  _ZL27bookerly_14_italicIntervals
    00000300 (   0.75 KB)  _ZL27bookerly_12_italicIntervals
    00000300 (   0.75 KB)  _ZL25bookerly_18_boldIntervals
    00000300 (   0.75 KB)  _ZL25bookerly_16_boldIntervals
    00000300 (   0.75 KB)  _ZL25bookerly_14_boldIntervals
    00000300 (   0.75 KB)  _ZL25bookerly_12_boldIntervals
    000002a0 (   0.66 KB)  small_prime
    000002a0 (   0.66 KB)  _ZL35opendyslexic_14_bolditalicIntervals
    000002a0 (   0.66 KB)  _ZL35opendyslexic_12_bolditalicIntervals
    000002a0 (   0.66 KB)  _ZL35opendyslexic_10_bolditalicIntervals
    000002a0 (   0.66 KB)  _ZL34opendyslexic_8_bolditalicIntervals
    000002a0 (   0.66 KB)  _ZL32opendyslexic_14_regularIntervals
    000002a0 (   0.66 KB)  _ZL32opendyslexic_12_regularIntervals
    000002a0 (   0.66 KB)  _ZL32opendyslexic_10_regularIntervals
    000002a0 (   0.66 KB)  _ZL31opendyslexic_8_regularIntervals
    000002a0 (   0.66 KB)  _ZL31opendyslexic_14_italicIntervals
    000002a0 (   0.66 KB)  _ZL31opendyslexic_12_italicIntervals
    000002a0 (   0.66 KB)  _ZL31opendyslexic_10_italicIntervals
    000002a0 (   0.66 KB)  _ZL30opendyslexic_8_italicIntervals
    000002a0 (   0.66 KB)  _ZL29opendyslexic_14_boldIntervals
    000002a0 (   0.66 KB)  _ZL29opendyslexic_12_boldIntervals
    000002a0 (   0.66 KB)  _ZL29opendyslexic_10_boldIntervals
    000002a0 (   0.66 KB)  _ZL28opendyslexic_8_boldIntervals
    00000280 (   0.62 KB)  K
    000001c8 (   0.45 KB)  _ZL26ubuntu_12_regularIntervals
    000001c8 (   0.45 KB)  _ZL26ubuntu_10_regularIntervals
    000001c8 (   0.45 KB)  _ZL23ubuntu_12_boldIntervals
    000001c8 (   0.45 KB)  _ZL23ubuntu_10_boldIntervals
    0000016c (   0.36 KB)  utf8_encoding_ns
    0000016c (   0.36 KB)  utf8_encoding
    0000016c (   0.36 KB)  little2_encoding_ns
    0000016c (   0.36 KB)  little2_encoding
    0000016c (   0.36 KB)  latin1_encoding_ns
    0000016c (   0.36 KB)  latin1_encoding
    0000016c (   0.36 KB)  internal_utf8_encoding_ns
    0000016c (   0.36 KB)  internal_utf8_encoding
    0000016c (   0.36 KB)  big2_encoding_ns
    0000016c (   0.36 KB)  big2_encoding
    0000016c (   0.36 KB)  ascii_encoding_ns
    0000016c (   0.36 KB)  ascii_encoding
    0000016c (   0.36 KB)  __default_global_locale
    00000150 (   0.33 KB)  oid_sig_alg
    00000150 (   0.33 KB)  mbedtls_cipher_definitions
    00000140 (   0.31 KB)  adc_error_coef_atten
    00000140 (   0.31 KB)  NUM_ERROR_CORRECTION_CODEWORDS
    0000012c (   0.29 KB)  _ZL11lookupTable
    00000101 (   0.25 KB)  _ctype_
    00000100 (   0.25 KB)  unhex
    00000100 (   0.25 KB)  tokens
    00000100 (   0.25 KB)  nmstrtPages
    00000100 (   0.25 KB)  namePages
    00000100 (   0.25 KB)  __chclass
    00000100 (   0.25 KB)  FSb4
    00000100 (   0.25 KB)  FSb3
    00000100 (   0.25 KB)  FSb2
    00000100 (   0.25 KB)  FSb
    000000fc (   0.25 KB)  _C_time_locale
    000000f0 (   0.23 KB)  oid_ecp_grp
    000000d4 (   0.21 KB)  _ZL8mapTable
    000000c8 (   0.20 KB)  __mprec_tens
    000000c0 (   0.19 KB)  dh_group5_prime
    000000c0 (   0.19 KB)  dh_group5_order
    000000b4 (   0.18 KB)  _ZL31notosans_18_bolditalicIntervals
    000000b4 (   0.18 KB)  _ZL31notosans_16_bolditalicIntervals
    000000b4 (   0.18 KB)  _ZL31notosans_14_bolditalicIntervals
    000000b4 (   0.18 KB)  _ZL31notosans_12_bolditalicIntervals
    000000b4 (   0.18 KB)  _ZL28notosans_18_regularIntervals

============================================
Top 40 largest symbols in section: .flash.text
Total section size: 1431082 bytes (1397.54 KB)
============================================
    000025b8 (   9.43 KB)  http_parser_execute
    000023aa (   8.92 KB)  _vfprintf_r
    000022ce (   8.70 KB)  _svfprintf_r
    0000225a (   8.59 KB)  _svfwprintf_r
    00001fdc (   7.96 KB)  __ssvfscanf_r
    00001cb0 (   7.17 KB)  _Z15getSettingsListv
    00001bbc (   6.93 KB)  mbedtls_ssl_handshake_server_step
    00001ac2 (   6.69 KB)  __ssvfiscanf_r
    000018fe (   6.25 KB)  mbedtls_ssl_handshake_client_step
    000015fe (   5.50 KB)  mdns_parse_packet
    00001554 (   5.33 KB)  _vfiprintf_r
    0000146e (   5.11 KB)  _svfiprintf_r
    0000123a (   4.56 KB)  doProlog
    0000101e (   4.03 KB)  tcp_input
    0000100e (   4.01 KB)  unsignedCharToPrintable
    00000f4e (   3.83 KB)  pjpeg_decode_mcu
    00000ef6 (   3.74 KB)  nd6_input
    00000d4a (   3.32 KB)  _dtoa_r
    00000d44 (   3.32 KB)  little2_contentTok
    00000d44 (   3.32 KB)  big2_contentTok
    00000d36 (   3.30 KB)  _strtod_l
    00000d30 (   3.30 KB)  mbedtls_high_level_strerr
    00000cec (   3.23 KB)  ieee80211_sta_new_state
    00000ca6 (   3.16 KB)  tcp_receive
    00000c82 (   3.13 KB)  mbedtls_internal_sha512_process
    00000c14 (   3.02 KB)  _ZN9WebServer10_parseFormER10WiFiClient6Stringm
    00000bc0 (   2.94 KB)  qrcode_initBytes
    00000b62 (   2.85 KB)  __multf3
    00000b1c (   2.78 KB)  normal_contentTok
    00000a98 (   2.65 KB)  _ZN16ContentOpfParser12startElementEPvPKcPS2_
    00000a96 (   2.65 KB)  _ZN18JpegToBmpConverter27jpegFileToBmpStreamInternalER6FsFileR5Printiibb
    00000a82 (   2.63 KB)  _mdns_service_task
    00000a64 (   2.60 KB)  __strftime
    00000a64 (   2.60 KB)  _ZNK9BaseTheme19drawRecentBookCoverER11GfxRenderer4RectRKSt6vectorI10RecentBookSaIS4_EEiRbS9_S9_St8functionIFbvEE
    00000a60 (   2.59 KB)  __strftime
    000009cc (   2.45 KB)  _Z16start_ssl_clientP17sslclient_contextRK9IPAddressmPKciS5_bS5_S5_S5_S5_bPS5_
    000009c4 (   2.44 KB)  doContent
    0000099a (   2.40 KB)  wpa_sm_rx_eapol
    00000984 (   2.38 KB)  __divtf3
    00000974 (   2.36 KB)  _ZNSt6locale5_ImplC2Ej

============================================
Top 10 largest symbols in section: .iram0.text
Total section size: 57640 bytes (56.29 KB)
============================================
    00000668 (   1.60 KB)  rmt_driver_isr_default
    00000504 (   1.25 KB)  tlsf_realloc
    00000458 (   1.09 KB)  tlsf_free
    000003e8 (   0.98 KB)  tlsf_malloc
    000003d0 (   0.95 KB)  esp_sleep_start
    00000340 (   0.81 KB)  rtc_sleep_init
    00000218 (   0.52 KB)  spi_flash_mmap_pages
    000001fc (   0.50 KB)  esp_flash_erase_region
    000001fc (   0.50 KB)  call_start_cpu0
    000001de (   0.47 KB)  wdt_hal_init
```

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **YES**
2026-02-19 10:35:07 -05:00
Bram Schulting
a8f0d63693 feat: Tweak Lyra popup UI (#768)
I want to preface this PR by stating that the proposed changes are
subjective to people's opinions. The following is just my suggestion,
but I'm of course open to changes.

The popups in the currently implemented version of the Lyra theme feel a
bit out of place. This PR suggests an updated version which looks a bit
more polished and in line with the rest of the theme.

I've also taken the liberty to remove the ellipsis behind the text of
the popups, as they made the popup feel a bit off balance (example
below).

With the applied changes, popups will look like this.

![IMG_0012](https://github.com/user-attachments/assets/a954de12-97b8-4102-be17-a702c0fe7d1e)

The vertical position is (more or less) aligned to be in line with the
sleep button. I'm aware the popup is used for other purposes aside from
the sleep message, but this still felt like a good place. It's also a
place where your eyes naturally 'rest'.

The popup has a small 2px white outline, neatly separating it from
whatever is behind it.

Initially I started out worked off the Figma design for the Lyra theme,
which [moves the
popups](https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2011-19296&t=Ppj6B2MrFRfUo9YX-1)
to the bottom of the screen. To me, this results in popups that are much
too easy to miss:

![IMG_0006](https://github.com/user-attachments/assets/b8ce3632-94a9-494e-8256-d87a6ee60cdf)

After this, I tried moving the popup back up (to the position of the
sleep button), but to me it still kinda disappeared into the text of the
book:

![IMG_0008](https://github.com/user-attachments/assets/4b05df7c-932e-432b-9c10-130da3109050)

Inverting the colors of the popup made things stand out the perfect
amount in my opinion. The white outline separates the popup from what is
behind it.

![IMG_0011](https://github.com/user-attachments/assets/77b1e8cc-0a57-4f4b-9abb-a9d10988d919)

This looked much better to me. The only thing that felt a bit off to me,
was the balance due to the ellipsis at the end of the popup text. Also,
"Entering Sleep..." felt a bit.. engineer-y. I felt something a bit more
'conversational' makes at all feel a bit more human-centric. But I'm no
copywriter, and English is not even my native language. So feel free to
chip in!

After tweaking that, I ended up with the final result:

_(Same picture as the first one shown in this PR)_

![IMG_0012](https://github.com/user-attachments/assets/a954de12-97b8-4102-be17-a702c0fe7d1e)

* Figma design:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2011-19296&t=Ppj6B2MrFRfUo9YX-1

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:34:58 -05:00
CaptainFrito
a8a89e35b8 feat: Lyra Icons (#725)
/!\ This PR depends on
https://github.com/crosspoint-reader/crosspoint-reader/pull/732 being
merged first

Also requires the
https://github.com/open-x4-epaper/community-sdk/pull/18 PR

Lyra theme icons on the home menu, in the file browser and on empty book
covers

![IMG_8023
Medium](https://github.com/user-attachments/assets/ba7c1407-94d2-4353-80ff-d5b800c6ac5b)
![IMG_8024
Medium](https://github.com/user-attachments/assets/edb59e13-b1c9-4c86-bef3-c61cc8134e64)
![IMG_7958
Medium](https://github.com/user-attachments/assets/d3079ce1-95f0-43f4-bbc7-1f747cc70203)
![IMG_8033
Medium](https://github.com/user-attachments/assets/f3e2e03b-0fa8-47b7-8717-c0b71361b7a8)

- Added a function to the open-x4-sdk renderer to draw transparent
images
- Added a scripts/convert_icon.py script to convert svg/png icons into a
C array that can be directly imported into the project. Usage:
```bash
python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32
```
This will create a components/icons/cover.h file with a C array called
CoverIcon, of size 32x32px. Lyra uses icons from
https://lucide.dev/icons with a stroke width of 2px, that can be
downloaded with any desired size on the site.

> The file browser is noticeably slower with the addition of icons, and
using an image buffer like on the home page doesn't help very much. Any
suggestions to optimize this are welcome.

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
The icon conversion python script was generated by Copilot as I am not a
python dev.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-19 10:33:23 -05:00
CaptainFrito
724c1969b9 feat: Lyra screens (#732)
Implements Lyra theme for some more Crosspoint screens:

![IMG_7960
Medium](https://github.com/user-attachments/assets/5d97d91d-e5eb-4296-bbf4-917e142d9095)
![IMG_7961
Medium](https://github.com/user-attachments/assets/02d61964-2632-45ff-83c7-48b95882eb9c)
![IMG_7962
Medium](https://github.com/user-attachments/assets/cf42d20f-3a85-4669-b497-1cac4653fa5a)
![IMG_7963
Medium](https://github.com/user-attachments/assets/a8f59c37-db70-407c-a06d-3e40613a0f55)
![IMG_7964
Medium](https://github.com/user-attachments/assets/0fdaac72-077a-48f6-a8c5-1cd806a58937)
![IMG_7965
Medium](https://github.com/user-attachments/assets/5169f037-8ba8-4488-9a8a-06f5146ec1d9)

- A bit of refactoring for list scrolling logic

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-19 10:32:36 -05:00
Dave Allie
21b81bd177 Update Ukrainian hyphenation 2026-02-19 10:25:45 -05:00
saslv
b5c48af3b2 feat: Added Ukrainian language hyphenation support (#646)
* **What is the goal of this PR?**
  Add proper hyphenation support for the Ukrainian language.

* **What changes are included?**
  - Added Ukrainian hyphenation rules/dictionary

---

Did you use AI tools to help write this code? _**NO**_
2026-02-19 10:25:32 -05:00
cottongin
426a978e44 feat: silent pre-indexing with configurable status bar indicator
Port PR #979's silent pre-indexing and add an Indexing Display setting
(Popup / Status Bar Text / Status Bar Icon) so users can choose how
indexing feedback is shown.

Silent pre-indexing runs on text-only penultimate pages when a status
bar option is selected, with a standard requestUpdate to clear the
indicator. Image pages skip silent indexing to avoid e-ink grayscale
pipeline conflicts; the normal popup handles those transitions. Direct
chapter jumps always show the original small popup regardless of setting.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 09:30:29 -05:00
cottongin
a1ac11ab51 feat: port upstream PRs #852, #965, #972, #971, #977, #975
Port 6 upstream PRs (PR #939 was already ported):

- #852: Complete HalPowerManager with RAII Lock class, WiFi check in
  setPowerSaving, skipLoopDelay overrides for ClearCache/OtaUpdate,
  and power lock in Activity render task loops
- #965: Fix paragraph formatting inside list items by tracking
  listItemUntilDepth to prevent unwanted line breaks
- #972: Micro-optimizations: std::move in insertFont, const ref for
  getDataFromBook parameter
- #971: Remove redundant hasPrintableChars pre-rendering pass from
  EpdFont, EpdFontFamily, and GfxRenderer
- #977: Skip unsupported image formats before extraction, add
  PARSE_BUFFER_SIZE constant and chapter parse timing
- #975: Fix UITheme memory leak by replacing raw pointer with
  std::unique_ptr for currentTheme

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 15:45:06 -05:00
cottongin
7819cf0f77 fix: correct book card highlight padding by increasing tile height
Instead of shrinking the highlight strip (which clipped author text),
increase homeCoverTileHeight from 310 to 318 for proper bottom padding.
Revert double-padding subtraction in bottomH/bottomSectionHeight.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 11:32:04 -05:00
cottongin
3d7340ca6f fix: add bottom padding to home screen book card highlight
The selection highlight had uniform padding on the top, left, and right
but none on the bottom, causing descenders in the author text to clip
past the highlight edge.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 03:53:31 -05:00
cottongin
966fbef3d1 mod: add clock settings tab, timezone support, and clock size option
Fix clock persistence bug caused by stale legacy read in settings
deserialization. Add clock size setting (Small/Medium/Large) and
timezone selection with North American presets plus custom UTC offset.
Move all clock-related settings into a dedicated Clock tab, rename
"Home Screen Clock" to "Clock", and move minute-change detection to
main loop so the header clock updates on every screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 03:46:06 -05:00
cottongin
38a87298f3 mod: fix clock bugs, add NTP auto-sync, show clock in all headers
- Fix SetTimeActivity immediately dismissing by changing wasReleased to
  wasPressed for all button inputs (matching other subactivities)
- Extract NTP sync into shared TimeSync utility (startNtpSync,
  waitForNtpSync, stopNtpSync) and trigger non-blocking NTP sync on
  every WiFi connection
- Move clock rendering into drawHeader (BaseTheme + LyraTheme) so it
  appears on all screens with a header, positioned symmetrically with
  the battery icon (12px margin, same Y offset, SMALL_FONT_ID)
- Add per-minute auto-refresh on home screen so clock updates without
  button press
- Add RTC time debug log on boot to verify time persistence across
  deep sleep

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 02:13:10 -05:00
Uri Tauber
ab4540b26f Fix dangling pointer 2026-02-17 01:31:51 -05:00
cottongin
7e15c9835f feat: long-press Confirm to open Table of Contents directly
Skip the reader menu when long-pressing Confirm (700ms) to jump
straight to the chapter selection screen. Short press behavior
(opening the menu) is unchanged. Extracts shared openChapterSelection()
helper to eliminate duplicated construction across three call sites.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 01:22:49 -05:00
cottongin
7b3de29c59 mod: improve home screen with adaptive layouts, clock, and set time
- 1-book view: horizontal layout with cover left, title/author right
- 2-3 book view: fix cover stretching by preserving aspect ratio
- 0-book view: show "Choose something to read" placeholder
- Selection highlight now fully contains title and author text
- Add optional clock display in home screen header (AM/PM or 24H)
- Add "Home Screen Clock" setting under Display
- Add "Set Time" activity for manual clock setting via Settings
- Increase homeCoverTileHeight to 310 for title/author breathing room

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 00:46:05 -05:00
cottongin
1d7971ae60 mod: overhaul reader menu with long-press actions and quick toggles
Consolidate dictionary items: remove "Lookup Word History" and
"Delete Dictionary Cache" from the menu. Long-press on "Lookup Word"
opens history; delete-dict-cache is now a sentinel entry at the bottom
of the history list.

Replace "Reading Orientation" with "Toggle Portrait/Landscape" that
toggles between two configurable preferred orientations (new settings:
Preferred Portrait, Preferred Landscape). Long-press opens a manual
4-option orientation sub-menu.

Add "Toggle Font Size" menu item that cycles through font sizes and
applies on menu exit (with section re-layout).

Rename "Letterbox Fill" to "Override Letterbox Fill" and
"Sync Progress" to "Sync Reading Progress" in reader menu.

All long-press flows use ignoreNextConfirmRelease to prevent the
button release from triggering actions on the subsequent screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 18:45:46 -05:00
cottongin
61fb11cae3 feat: Add PNG cover image support for EPUB books (#827)
Cherry-pick upstream PR #827 with conflict resolution for mod/master:

- Add PngToBmpConverter library for PNG cover → BMP conversion
- Add PNG thumbnail generation in generateThumbBmp()
- Fix generateCoverBmp() PNG block to use effectiveCoverImageHref
  (consistent with mod's fallback cover candidate probing)
- Add .png to getCoverCandidates() extensions
- Use LOG_ERR macro in ImageToFramebufferDecoder (mod standard)
- Upstream image converter refinements (ImageBlock, PixelCache,
  JpegToFramebufferConverter, PngToFramebufferConverter)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 17:04:33 -05:00
Егор Мартынов
424e332c75 chore: improve Russian language support (#926)
## Summary

This PR includes vocabulary and grammar fixes for Russian translation,
originally made as review comments
[here](https://github.com/crosspoint-reader/crosspoint-reader/pull/728).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-16 17:00:43 -05:00
Zach Nelson
f21720dc79 perf: Skip constructing unnecessary std::string (#932)
## Summary

**What is the goal of this PR?**

Skip constructing a `std::string` just to get the underlying `c_str()`
buffer, when a string literal gives the same end result.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-16 17:00:32 -05:00
cottongin
a9f5149444 mod: remove duplicate I18n.h include in HomeActivity.cpp
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 13:26:17 -05:00
cottongin
0222cbf19b mod: convert remaining manual render locks to RAII RenderLock
Replace xSemaphoreTake/Give(renderingMutex) with scoped
RenderLock in EpubReaderActivity popup rendering (bookmark
added/removed, dictionary cache deleted).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 13:24:48 -05:00
cottongin
02f2474e3b mod: adapt mod activities to #774 render() pattern
Migrate 5 mod Activity subclasses from old polling-based
display task pattern to the upstream render() super-class
pattern with freeRTOS notification:

- EpubReaderBookmarkSelectionActivity
- DictionaryWordSelectActivity
- DictionarySuggestionsActivity
- DictionaryDefinitionActivity
- LookedUpWordsActivity

Changes: remove own TaskHandle/SemaphoreHandle/updateRequired,
use requestUpdate() + render(RenderLock&&) override, fix
potential deadlocks around enterNewActivity() calls.

Also fix stale conflict marker in EpubReaderMenuActivity.h.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 13:22:40 -05:00
pablohc
f06e3a0a82 fix: align battery icon based on context (UI / Reader) (#796)
Issues solved: #729 and #739

## Summary

* **What is the goal of this PR?**
Currently, the battery icon and charge percentage were aligned to the
left even for the UI, where they were positioned on the right side of
the screen. This meant that when changing values of different numbers of
digits, the battery would shift, creating a block of icons and text that
was illegible.

* **What changes are included?**
- Add drawBatteryUi() method for right-aligned battery display in UI
headers
- Keep drawBattery() for left-aligned display in reader mode
- Extract drawBatteryIcon() helper to reduce code duplication
- Battery icon now stays fixed at right edge regardless of percentage
digits
- Text adjusts to left of icon in UI mode, to right of icon in reader
mode

## Additional Context

* Add any other information that might be helpful for the reviewer 
* This fix applies to both themes (Base and Lyra).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
2026-02-16 13:14:44 -05:00
Andrew Brandt
a585f219f4 docs: add translators doc (#792)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Add a translators document for us to track which individuals have
volunteered to contribute in which languages.

* **What changes are included?**
Add a new document that includes who the translators are and what
languages they have volunteered for.

## Additional Context

This is primarily to keep a handle on the volunteers coming into the
repo. This will serve as a master list of all volunteer translators.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

---------

Signed-off-by: Andrew Brandt <brandt.andrew89@gmail.com>
2026-02-16 13:14:30 -05:00
Lev Roland-Kalb
df6cc637ec docs: Updating webserver.md documentation to align with 1.0.0 features (#906)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Updating webserver.md documentation to align with 1.0.0 features

* **What changes are included?**

Added documentation for the following new features (including replacing
screenshots)

- file renaming
- file moving
- support for uploading any file type
- batch uploads

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

Nothing comes to mind

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-16 13:13:05 -05:00
Lev Roland-Kalb
4cfe155488 fix: Removed white boxes extending passed the bounds of the empty button icon when hint text is blank/null (#884)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Empty Button Icons (I.E. Back button in the home menu) were still
rendering the full sized white rectangles going passed the boarders of
the little button nub. This was not visible on the home screen due to
the white background, but it does cause issues if we ever want to have
bmp files displayed while buttons are visible or implement a dark mode.

* **What changes are included?**

Made it so that when a button hint text is empty string or null the
displayed mini button nub does not have a white rectangle extending
passed the bounds of the mini button nub

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

Having that extended rectangle was likely never noticed due to the only
space where that feature is used being the main menu where the
background is completely white. I am working on some new features that
would have an image displayed while there are button hints and noticed
this issue while implementing that.

One other note is that this only affects the Lyra Theme

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
2026-02-16 13:12:50 -05:00
Uri Tauber
f1966f1e26 feat: User-Interface I18n System (#728)
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.

**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.

- Data Structures:

- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
  - `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
  - `lib/I18n/translations.csv`: single source of truth.

- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.

- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`

This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.

The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.

The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.

To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.

- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.

Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!

**Status:** EDIT: ready to be merged.

As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.

---

Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
2026-02-16 13:12:29 -05:00
Xuan-Son Nguyen
ebcd3a8b94 fix: use RAII render lock everywhere (#916)
Follow-up to
https://github.com/crosspoint-reader/crosspoint-reader/pull/774

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **Refactor**
* Modernized internal synchronization mechanisms across multiple
components to improve code reliability and maintainability. All
functionality remains unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 13:03:50 -05:00
Xuan-Son Nguyen
ed8a0feac1 refactor: move render() to Activity super class, use freeRTOS notification (#774)
Currently, each activity has to manage their own `displayTaskLoop` which
adds redundant boilerplate code. The loop is a wait loop which is also
not the best practice, as the `updateRequested` boolean is not protected
by a mutex.

In this PR:
- Move `displayTaskLoop` to the super `Activity` class
- Replace `updateRequested` with freeRTOS's [direct to task
notification](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/03-Direct-to-task-notifications/01-Task-notifications)
- For `ActivityWithSubactivity`, whenever a sub-activity is present, the
parent's `render()` automatically goes inactive

With this change, activities now only need to expose `render()`
function, and anywhere in the code base can call `requestUpdate()` to
request a new rendering pass.

In theory, this change may also make the battery life a bit better,
since one wait loop is removed. Although the equipment in my home lab
wasn't been able to verify it (the electric current is too noisy and
small). Would appreciate if anyone has any insights on this subject.

Update: I managed to hack [a small piece of
code](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage)
that allow tracking CPU idle time.

The CPU load does decrease a bit (1.47% down to 1.39%), which make
sense, because the display task is now sleeping most of the time unless
notified. This should translate to a slightly increase in battery life
in the long run.

```
PR:
[40012] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[40012] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[50017] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[50017] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[60022] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[60022] [IDLE] Idle time: 98.61% (CPU load: 1.39%)

master:
[20012] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[20012] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[30017] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[30017] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[40022] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[40022] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
```

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **Refactor**
* Streamlined rendering architecture by consolidating update mechanisms
across all activities, improving efficiency and consistency.
* Modernized synchronization patterns for display updates to ensure
reliable, conflict-free rendering.

* **Bug Fixes**
* Enhanced rendering stability through improved locking mechanisms and
explicit update requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: znelson <znelson@users.noreply.github.com>
2026-02-16 13:01:42 -05:00
jpirnay
12cc7de49e fix: Add miniz directive to get rid of compilation warning (#858)
## Summary

* I am getting miniz warning during compilation: "Using fopen, ftello,
fseeko, stat() etc. path for file I/O - this path may not support large
files."
* Disable the io module from miniz as it is not used and get rid of the
warning

## Additional Context

* the ZipFile.cpp implementation only uses tinfl_decompressor,
tinfl_init(), and tinfl_decompress() (low-level API) and does all ZIP
file parsing manually using SD card file I/O
* it never uses miniz's high-level file functions like
mz_zip_reader_init_file()
* so we can disable Miniz io-stack be setting MINIZ_NO_STDIO to 1

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? partially, let claude
inspect the codebase
2026-02-16 12:43:25 -05:00
jpirnay
f622e87c10 fix: Correct multiple author display (#856)
## Summary

* If an EPUB has:
```
<dc:creator>J.R.R. Tolkien</dc:creator>
<dc:creator>Christopher Tolkien</dc:creator>
```
the current result for epub.author would provide : "J.R.R.
TolkienChristopher Tolkien" (no separator!)
* The fix will seperate multiple authors: "J.R.R. Tolkien, Christopher
Tolkien"

## Additional Context

* Simple fix in ContentOpfParser - I am not seeing any dependence on the
wrong concatenated result.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO
2026-02-16 12:43:13 -05:00
Dave Allie
24c1df0308 docs: Include dictionary as in-scope (#917)
## Summary

* Include dictionary as in-scope

## Additional Context

* Discussion in
https://github.com/crosspoint-reader/crosspoint-reader/discussions/878

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-02-16 12:43:02 -05:00
ThatCrispyToast
6cc68e828a fix: add distro agnostic shebang and clang-format check to clang-format-fix (#840)
## Summary

**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Minor development tooling fix for nonstandard environments (NixOS,
FreeBSD, Guix, etc.)

**What changes are included?**

- environment relative shebang in `clang-format-fix`
- clang-format check in `clang-format-fix`
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-16 12:42:47 -05:00
jpirnay
6097ee03df fix: Auto calculate the settings size on serialization (#832)
* The constant SETTINGS_CONST was hardcoded and needed to be updated
whenever an additional setting was added
* This is no longer necessary as the settings size will be determined
automatically on settings persistence

* New settings need to be added (as previously) in saveToFile - that's
it

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? YES

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-02-16 12:42:35 -05:00
Xuan-Son Nguyen
d11ad45e59 perf: apply (micro) optimization on SerializedHyphenationPatterns (#689)
This PR applies a micro optimization on `SerializedHyphenationPatterns`,
which allow reading `rootOffset` directly without having to parse then
cache it.

It should not affect storage space since no new bytes are added.

This also gets rid of the linear cache search whenever
`liangBreakIndexes` is called. In theory, the performance should be
improved a bit, although it may be too small to be noticeable in
practice.

master branch:

```
english: 99.1023%
french: 100%
german: 97.7289%
russian: 97.2167%
spanish: 99.0236%
```

This PR:

```
english: 99.1023%
french: 100%
german: 97.7289%
russian: 97.2167%
spanish: 99.0236%
```

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? PARTIALLY - mostly IDE
tab-autocompletions
2026-02-16 12:39:23 -05:00
cottongin
b965ce9fb7 fix: Port upstream cover extraction fallback and outline improvements
Port PR #838 (epub cover fallback logic) and PR #907 (cover outlines):

- Add fallback cover filename probing when EPUB metadata lacks cover info
- Case-insensitive extension checking for cover images
- Detect and re-generate corrupt/empty thumbnail BMPs
- Always draw outline rect on cover tiles for legibility (PR #907)
- Upgrade Storage.exists() checks to Epub::isValidThumbnailBmp()
- Fallback chain: Real Cover → PlaceholderCoverGenerator → X-pattern marker
- Add epub.load retry logic (cache-only first, then full build)
- Adapt upstream Serial.printf calls to LOG_DBG/LOG_ERR macros

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 01:20:27 -05:00
cottongin
744d6160e8 Merge branch 'master' into mod/master-img
Merge upstream perf: Improve large CSS files handling (#779)

Conflicts resolved:
- Section.cpp: Combined mod's image support variables with master's
  CSS parser loading pattern
- CssParser.cpp: Accepted master's streaming parser rewrite, ported
  mod's width property handler into parseDeclarationIntoStyle()

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 20:36:48 -05:00
cottongin
66f703df69 fix: Fix cover thumbnail pipeline for home screen
Remove empty sentinel BMP file from generateThumbBmp() that blocked
placeholder generation for books without covers. Add removeBook() to
RecentBooksStore and clear book from recents on cache delete. Ensure
home screen always generates placeholder when thumbnail generation fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 19:53:52 -05:00
cottongin
19004eefaa feat: Add EPUB embedded image support (JPEG/PNG)
Cherry-pick merge from pablohc/crosspoint-reader@2d8cbcf, based on
upstream PR #556 by martinbrook with pablohc's refresh optimization.

- Add JPEG decoder (picojpeg) and PNG decoder (PNGdec) with 4-level
  grayscale Bayer dithering for e-ink display
- Add pixel caching system (.pxc files) for fast image re-rendering
- Integrate image extraction from EPUB HTML parser (<img> tag support)
- Add ImageBlock/PageImage types with serialization support
- Add image-aware refresh optimization (double FAST_REFRESH technique)
- Add experimental displayWindow() partial refresh support
- Bump section cache version 12->13 to invalidate stale caches
- Resolve TAG_PageImage=3 to avoid conflict with mod's TAG_PageTableRow=2

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 17:29:39 -05:00
cottongin
f90aebc891 fix: Defer low-power mode during section indexing and book loading
Prevent the device from dropping to 10MHz CPU during first-time chapter
indexing, cover prerendering, and other CPU-intensive reader operations.

Three issues addressed:
- ActivityWithSubactivity now delegates preventAutoSleep() and
  skipLoopDelay() to the active subactivity, so EpubReaderActivity's
  signal is visible through the ReaderActivity wrapper
- Added post-loop() re-check of preventAutoSleep() in main.cpp to
  catch activity transitions that happen mid-loop
- EpubReaderActivity uses both !section and a loadingSection flag to
  cover the full duration from activity entry through section file
  creation; TxtReaderActivity uses !initialized similarly

Also syncs HalPowerManager.cpp log messages with upstream PR #852.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 16:42:27 -05:00
cottongin
3096d6066b feat: Add column-aligned table rendering for EPUBs
Replace the "[Table omitted]" placeholder with full table rendering:

- Two-pass layout: buffer table content during SAX parsing, then
  calculate column widths and lay out cells after </table> closes
- Colspan support for cells spanning multiple columns
- Forced line breaks within cells (<br>, <p>, <div> etc.)
- Center-align full-width spanning rows (section headers/titles)
- Width hints from HTML attributes and CSS (col, td, th width)
- Two-pass fair-share column width distribution that prevents
  narrow columns from being excessively squeezed
- Double-encoded &nbsp; entity handling
- PageTableRow with grid-line rendering and serialization support
- Asymmetric vertical cell padding to balance font leading

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 14:40:36 -05:00
cottongin
1383d75c84 feat: Add per-family font and per-language hyphenation build flags
Add OMIT_BOOKERLY, OMIT_NOTOSANS, OMIT_OPENDYSLEXIC flags to
selectively exclude font families, and OMIT_HYPH_DE/EN/ES/FR/IT/RU
flags to exclude individual hyphenation language tries.

The mod build environment excludes OpenDyslexic (~1.03 MB) and all
hyphenation tries (~282 KB), reducing flash usage by ~1.3 MB.

Font Family setting switched from Enum to DynamicEnum with
index-to-value mapping to handle arbitrary font exclusion without
breaking the settings UI or persisted values.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 00:48:23 -05:00
cottongin
632b76c9ed feat: Add placeholder cover generator for books without covers
Generate styled placeholder covers (title, author, book icon) when a
book has no embedded cover image, instead of showing a blank rectangle.

- Add PlaceholderCoverGenerator lib with 1-bit BMP rendering, scaled
  fonts, word-wrap, and a book icon bitmap
- Integrate as fallback in Epub/Xtc/Txt reader activities and
  SleepActivity after format-specific cover generation fails
- Add fallback in HomeActivity::loadRecentCovers() so the home screen
  also shows placeholder thumbnails when cache is cleared
- Add Txt::getThumbBmpPath() for TXT thumbnail support
- Add helper scripts for icon and layout preview generation

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 23:38:47 -05:00
cottongin
5dc9d21bdb feat: Integrate PR #857 dictionary intelligence and sub-activity refactor
Pull in the full feature update from PR #857 while preserving fork
advantages (HTML parsing, custom drawHints, PageForward/PageBack,
cache management, stardictCmp, /.dictionary/ paths).

- Add morphological stemming (getStemVariants), Levenshtein edit
  distance, and fuzzy matching (findSimilar) to Dictionary
- Create DictionarySuggestionsActivity for "Did you mean?" flow
- Add onDone callback to DictionaryDefinitionActivity for direct
  exit-to-reader via "Done" button
- Refactor DictionaryWordSelectActivity to ActivityWithSubactivity
  with cascading lookup (exact → stems → suggestions → not found),
  en-dash/em-dash splitting, and cross-page hyphenation
- Refactor LookedUpWordsActivity with reverse-chronological order,
  inline cascading lookup, UITheme-aware rendering, and sub-activities
- Simplify EpubReaderActivity LOOKUP/LOOKED_UP_WORDS handlers

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 20:50:03 -05:00
cottongin
c1dfe92ea3 Merge remote-tracking branch 'upstream/master' into mod/master 2026-02-14 19:53:57 -05:00
cottongin
82bfbd8fa6 merge upstream/master: logging pragma, screenshot retrieval, nbsp fix
Merge 3 upstream commits into mod/master:
- feat: Allow screenshot retrieval from device (#820)
- feat: Add central logging pragma (#843)
- fix: Account for nbsp character as non-breaking space (#757)

Conflict resolution:
- src/main.cpp: kept mod's HalPowerManager + upstream's Logging/screenshot
- SleepActivity.cpp: kept mod's letterbox fill rework, applied LOG_* pattern

Additional changes for logging compatibility:
- Converted remaining Serial.printf calls in mod files to LOG_* macros
  (HalPowerManager, BookSettings, BookmarkStore, GfxRenderer)
- Added ENABLE_SERIAL_LOG and LOG_LEVEL=2 to [env:mod] build flags

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 16:27:58 -05:00
cottongin
6aa0b865c2 feat: Add per-book letterbox fill override
Introduce BookSettings utility for per-book settings stored in
the book's cache directory (book_settings.bin). Add "Letterbox Fill"
option to the EPUB reader menu that cycles Default/Dithered/Solid/None.
At sleep time, the per-book override is loaded and takes precedence
over the global setting for all book types (EPUB, XTC, TXT).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 16:07:38 -05:00
cottongin
0c71e0b13f fix: Use hash-based block dithering for BW-boundary letterbox fills
Pixel-level Bayer dithering in the 171-254 gray range creates a
high-frequency checkerboard in the BW pass that causes e-ink display
crosstalk during HALF_REFRESH, washing out cover images. Replace with
2x2 hash-based block dithering for this specific gray range — each
block gets a uniform level (2 or 3) via a spatial hash, avoiding
single-pixel alternation while approximating the target gray. Standard
Bayer dithering remains for all other gray ranges.

Also removes all debug instrumentation from the investigation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 14:49:42 -05:00
cottongin
ea11d2f7d3 refactor: Revert letterbox fill to Dithered/Solid/None with edge caching
Simplify letterbox fill modes back to Dithered (default), Solid, and
None. Remove the Extend Edges mode and all per-pixel edge replication
code. Restore Bayer ordered dithering for the Dithered fill mode.

Re-introduce edge average caching so cover edge computations persist
across sleep cycles, stored as a small binary file alongside the cover
BMP.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 11:12:27 -05:00
cottongin
31878a77bc feat: Add mod build environment with version + git hash
Add `env:mod` PlatformIO environment that sets CROSSPOINT_VERSION to
"{version}-mod+{git_hash}" via a pre-build script. Usage: `pio run -e mod -t upload`

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 20:57:55 -05:00
cottongin
21a75c624d feat: Implement bookmark functionality for epub reader
Replace bookmark stubs with full add/remove/navigate implementation:

- BookmarkStore: per-book binary persistence on SD card with v2 format
  supporting text snippets (backward-compatible with v1)
- Visual bookmark ribbon indicator drawn on bookmarked pages via fillPolygon
- Reader menu dynamically shows Add/Remove Bookmark based on current page state
- Bookmark selection activity with chapter name, first sentence snippet, and
  page number display; long-press to delete with confirmation
- Go to Bookmark falls back to Table of Contents when no bookmarks exist
- Smart snippet extraction: skips partial sentences (lowercase first word)
  to capture the first full sentence on the page
- Label truncation reserves space for page suffix so it's never cut off
- Half refresh forced on menu exit to clear popup/menu artifacts

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 20:40:07 -05:00
cottongin
8d4bbf284d feat: Add dictionary word lookup feature with cached index
Implements StarDict-based dictionary lookup from the reader menu,
adapted from upstream PR #857 with /.dictionary/ folder path,
std::vector compatibility (PR #802), HTML definition rendering,
orientation-aware button hints, side button hints with CCW text
rotation, sparse index caching to SD card, pronunciation line
filtering, and reorganized reader menu with bookmark stubs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 19:36:14 -05:00
cottongin
905f694576 prerender book covers and thumbnails when opening a book for the first time
Moves cover/thumbnail generation from lazy (Home screen, Sleep screen) into
each reader activity's onEnter(). On first open, generates all needed BMPs
(cover, cover_crop, thumbnails for all theme heights) with a "Preparing
book..." progress popup. Subsequent opens skip instantly when files exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:13:55 -05:00
cottongin
e798065a5c merge upstream PR #852: feat: lower CPU freq on idle, add HalPowerManager 2026-02-12 12:09:20 -05:00
cottongin
5e269f912f merge upstream PR #802: perf: Replace std::list with std::vector in text layout 2026-02-12 12:09:10 -05:00
cottongin
182c236050 Merge branch 'master' into mod/master
Resolve single conflict in SleepActivity.cpp: adopt upstream millis()
timestamp log format while preserving mod's edgeCachePath argument to
renderBitmapSleepScreen().

Upstream changes (14 commits): unified navigation handling, Italian
hyphenation, natural file sort, auto WiFi reconnect, power saving on
idle, OPDS fixes, uniform debug logging, and more.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 11:54:40 -05:00
Xuan Son Nguyen
73cd05827a move IDLE_POWER_SAVING_MS 2026-02-12 13:19:37 +01:00
Xuan Son Nguyen
ea32ba0f8d add HalPowerManager 2026-02-12 13:12:13 +01:00
Xuan Son Nguyen
f7b1113819 Merge branch 'master' into xsn/idle_cpu_freq 2026-02-12 11:37:32 +01:00
Xuan Son Nguyen
228a1cb511 rm test 2026-02-12 11:37:12 +01:00
Xuan Son Nguyen
b72283d304 change cpu freq on idle 2026-02-10 23:27:45 +01:00
Xuan Son Nguyen
8cf226613b clang format 2026-02-10 14:19:16 +01:00
Xuan Son Nguyen
d4f25c44bf lower to 3 seconds 2026-02-10 11:31:28 +01:00
Kuanysh Bekkulov
bc12556da1 perf: Replace std::list with std::vector in TextBlock and ParsedText
Replace std::list with std::vector for the words, wordStyles,
wordXpos, and wordContinues containers in TextBlock and ParsedText.

Vectors provide contiguous memory layout for better cache locality
and O(1) random access, eliminating per-node heap allocation and
the 16-byte prev/next pointer overhead of doubly-linked list nodes.
The indexed access also removes the need for a separate continuesVec
copy that was previously built from the list for O(1) layout access.
2026-02-09 23:46:08 +05:00
Xuan Son Nguyen
4e7bb8979c revert test 2026-02-09 19:20:36 +01:00
cottongin
4edb14bdd9 feat: Sleep screen letterbox fill and image upscaling
Some checks failed
CI (build) / clang-format (push) Has been cancelled
CI (build) / cppcheck (push) Has been cancelled
CI (build) / build (push) Has been cancelled
CI (build) / Test Status (push) Has been cancelled
Add configurable letterbox fill for sleep screen cover images that don't
match the display aspect ratio. Four fill modes are available: Solid
(single dominant edge shade), Blended (per-pixel edge colors), Gradient
(edge colors interpolated toward white/black), and None.

Enable upscaling of cover images smaller than the display in Fit mode by
modifying drawBitmap/drawBitmap1Bit to support both up and downscaling
via a unified block-fill approach.

Edge sampling data is cached to .crosspoint alongside the cover BMP to
avoid redundant bitmap scanning on subsequent sleeps. Cache is validated
against screen dimensions and auto-regenerated when stale.

New settings: Letterbox Fill (None/Solid/Blended/Gradient) and Gradient
Direction (To White/To Black).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 11:52:55 -05:00
Xuan Son Nguyen
eb79b98f2b power saving on idle 2026-02-09 12:45:16 +01:00
cottongin
a85d5e627b .gitignore tweaks for mod fork 2026-02-09 04:15:00 -05:00
256 changed files with 118451 additions and 915 deletions

View File

@@ -0,0 +1,171 @@
---
name: TTF Font Investigation
overview: Investigate replacing compile-time bitmap fonts with runtime TTF rendering using stb_truetype (the core of lvgl-ttf-esp32), integrated into the existing custom GfxRenderer pipeline for the ESP32-C3 e-ink reader.
todos:
- id: poc-stb
content: "Phase 1: Add stb_truetype.h and build a minimal proof-of-concept that loads a TTF from SD, rasterizes glyphs, and draws them via GfxRenderer"
status: pending
- id: measure-ram
content: "Phase 1: Measure actual RAM consumption and render performance of stb_truetype on ESP32-C3"
status: pending
- id: spiffs-mmap
content: "Phase 3: Test SPIFFS memory-mapping of TTF files using esp_partition_mmap() to avoid loading into RAM"
status: pending
- id: font-provider
content: "Phase 2: Create FontProvider abstraction layer and TtfFontProvider with glyph caching"
status: pending
- id: renderer-refactor
content: "Phase 2: Refactor GfxRenderer to use FontProvider interface instead of direct EpdFontFamily"
status: pending
- id: settings-integration
content: "Phase 4: Update settings to support arbitrary font sizes and custom font selection"
status: pending
- id: remove-bitmap-fonts
content: "Phase 5: Remove compiled bitmap reader fonts, keep only small UI bitmap fonts"
status: pending
isProject: false
---
# TTF Font Rendering Investigation
## Current State
The project uses **no LVGL** -- it has a custom `GfxRenderer` that draws directly into an e-ink framebuffer. Fonts are pre-rasterized offline (TTF -> Python FreeType script -> C header bitmaps) and embedded at compile time.
**Cost of current approach:**
- **~2.7 MB flash** (mod build, Bookerly + NotoSans + Ubuntu) up to **~7 MB** (full build with OpenDyslexic)
- **Only 4 discrete sizes** per family (12/14/16/18 pt) -- no runtime scaling
- Each size x style (regular/bold/italic/bold-italic) is a separate ~80-200 KB bitmap blob
- App partition is only **6.25 MB** -- fonts consume 43-100%+ of available space
## Why lvgl-ttf-esp32 Is Relevant (and What Isn't)
The [lvgl-ttf-esp32](https://github.com/huming2207/lvgl-ttf-esp32) repo wraps **stb_truetype** (a single-header C library) with an LVGL font driver. Since this project does not use LVGL, the wrapper is irrelevant, but the **stb_truetype library itself** is exactly what's needed -- a lightweight, zero-dependency TTF rasterizer that runs on ESP32.
## Proposed Architecture
```mermaid
flowchart TD
subgraph current [Current Pipeline]
TTF_Offline["TTF files (offline)"] --> fontconvert["fontconvert.py (FreeType)"]
fontconvert --> headers["56 .h files (~2.7-7 MB flash)"]
headers --> EpdFont["EpdFont / EpdFontFamily"]
EpdFont --> GfxRenderer["GfxRenderer::renderChar()"]
end
subgraph proposed [Proposed Pipeline]
TTF_SD["TTF files on SD card (~100-500 KB each)"] --> stb["stb_truetype.h (runtime)"]
stb --> cache["Glyph cache (RAM + SD)"]
cache --> TtfFont["TtfFont (new class)"]
TtfFont --> FontProvider["FontProvider interface"]
FontProvider --> GfxRenderer2["GfxRenderer::renderChar()"]
end
```
### Core Idea
1. **stb_truetype.h** -- add as a single header file in `lib/`. It rasterizes individual glyphs from TTF data on demand.
2. **TTF files on SD card** -- load at runtime from `.crosspoint/fonts/`. A typical TTF family (4 styles) is ~400-800 KB total vs 2.7 MB+ as bitmaps.
3. **Glyph cache** -- since e-ink pages are static, cache rasterized glyphs in RAM (LRU, ~20-50 KB) and optionally persist to SD card to avoid re-rasterizing across page turns.
4. `**FontProvider` abstraction** -- interface over both `EpdFont` (bitmap, for UI fonts) and new `TtfFont` (runtime, for reader fonts), so both can coexist.
## Integration Points
These are the key files/interfaces that would need changes:
| Component | File | Change |
| ----------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Font abstraction | New `lib/FontProvider/` | `FontProvider` interface with `getGlyph()`, `getMetrics()` |
| TTF renderer | New `lib/TtfFont/` | Wraps stb_truetype, manages TTF loading + glyph cache |
| GfxRenderer | [lib/GfxRenderer/GfxRenderer.h](lib/GfxRenderer/GfxRenderer.h) | Change `fontMap` from `EpdFontFamily` to `FontProvider*`; update `renderChar`, `getTextWidth`, `getSpaceWidth` |
| Font registration | [src/main.cpp](src/main.cpp) | Register TTF fonts from SD instead of (or alongside) bitmap fonts |
| Settings | [src/CrossPointSettings.cpp](src/CrossPointSettings.cpp) | `getReaderFontId()` supports arbitrary sizes, not just 4 discrete ones |
| PlaceholderCover | [lib/PlaceholderCover/PlaceholderCoverGenerator.cpp](lib/PlaceholderCover/PlaceholderCoverGenerator.cpp) | Uses own `renderGlyph()` -- needs similar adaptation |
| Text layout | [lib/Epub/Epub/ParsedText.cpp](lib/Epub/Epub/ParsedText.cpp) | Uses `getTextWidth()` / `getSpaceWidth()` for line breaking -- works unchanged if FontProvider is transparent |
## Feasibility Analysis
### Memory (ESP32-C3, ~380 KB RAM)
- **stb_truetype itself**: ~15-20 KB code in flash, minimal RAM overhead
- **TTF file in memory**: requires the full TTF loaded into RAM for glyph access. Options:
- **Memory-mapped from flash (SPIFFS)**: store TTF in SPIFFS (3.4 MB available, currently unused), memory-map via `mmap()` on ESP-IDF -- zero RAM cost
- **Partial loading from SD**: read only needed tables on demand (stb_truetype supports custom `stbtt_read()` but the default API expects full file in memory)
- **Load into PSRAM**: ESP32-C3 has no PSRAM, so this is not an option
- **Glyph cache**: ~50 bytes metadata + bitmap per glyph. At 18pt, a glyph bitmap is ~20x25 pixels = ~63 bytes (1-bit). Caching 256 glyphs = ~30 KB RAM.
- **Rasterization temp buffer**: stb_truetype allocates ~10-20 KB temporarily per glyph render (uses `malloc`)
**Verdict**: The biggest constraint is holding the TTF file in RAM. A typical Bookerly-Regular.ttf is ~150 KB. With 4 styles loaded, that's ~600 KB -- **too much for 380 KB RAM**. The viable path is using **SPIFFS** to store TTFs and memory-map them, or implementing a chunked reader that loads TTF table data on demand from SD.
### Flash Savings
- **Remove**: 2.7-7 MB of bitmap font headers from firmware
- **Add**: ~40 KB for stb_truetype + TtfFont code
- **Net savings**: **2.6-6.9 MB flash freed**
- TTF files move to SD card or SPIFFS (not in firmware)
### Performance
- stb_truetype rasterizes a glyph in **~0.5-2 ms** on ESP32 (160 MHz)
- A typical page has ~~200-300 glyphs, but with caching, only unique glyphs need rasterizing (~~60-80 per page)
- **First page render**: ~60-160 ms extra for cache warmup
- **Subsequent pages**: mostly cache hits, negligible overhead
- E-ink refresh takes ~300-1000 ms anyway, so TTF rasterization cost is acceptable
### Anti-aliasing for E-ink
stb_truetype produces 8-bit alpha bitmaps (256 levels). The current system uses 1-bit or 2-bit glyphs. The adapter would:
- **1-bit mode**: threshold the alpha (e.g., alpha > 128 = black)
- **2-bit mode**: quantize to 4 levels (0, 85, 170, 255) for e-ink grayscale
This should actually produce **better quality** than the offline FreeType conversion since stb_truetype does sub-pixel hinting.
## Recommended Implementation Phases
### Phase 1: Proof of Concept (stb_truetype standalone)
- Add stb_truetype.h to the project
- Write a minimal test that loads a TTF from SD, rasterizes a few glyphs, and draws them via `GfxRenderer::drawPixel()`
- Measure RAM usage and render time
- Validate glyph quality on e-ink
### Phase 2: FontProvider Abstraction
- Create `FontProvider` interface matching `EpdFontFamily`'s public API
- Wrap existing `EpdFontFamily` in a `BitmapFontProvider`
- Create `TtfFontProvider` backed by stb_truetype + glyph cache
- Refactor `GfxRenderer::fontMap` to use `FontProvider*`
### Phase 3: TTF Storage Strategy
- Evaluate SPIFFS memory mapping vs. SD-card chunked loading
- Implement the chosen strategy
- Handle font discovery (scan SD card for `.ttf` files)
### Phase 4: Settings and UI Integration
- Replace discrete font-size enum with a continuous size setting (or finer granularity)
- Add "Custom Font" option in settings
- Update section cache invalidation when font/size changes
### Phase 5: Remove Bitmap Reader Fonts
- Keep bitmap fonts only for UI (Ubuntu 10/12, NotoSans 8) which are small (~62 KB)
- Remove Bookerly, NotoSans, OpenDyslexic bitmap headers
- Ship TTF files on SD card (or downloadable)
## Key Risk: TTF-in-RAM on ESP32-C3
The critical question is whether TTF file data can be accessed without loading the full file into RAM. Three mitigation strategies:
1. **SPIFFS + mmap**: Store TTFs in the 3.4 MB SPIFFS partition and use ESP-IDF's `esp_partition_mmap()` to map them into the address space. Zero RAM cost, but SPIFFS is read-only after flashing (unless written at runtime).
2. **SD card + custom I/O**: Implement `stbtt_GetFontOffsetForIndex` and glyph extraction using buffered SD reads. stb_truetype's API assumes a contiguous byte array, so this would require a patched or wrapper approach.
3. **Load one style at a time**: Only keep the active style's TTF in RAM (~150 KB). Switch styles by unloading/reloading. Feasible but adds latency on style changes (bold/italic).
Strategy 1 (SPIFFS mmap) is the most promising since the SPIFFS partition is already allocated but unused.

View File

@@ -110,7 +110,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
- Only ONE framebuffer exists (not double-buffered)
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
- Must call `renderer.restoreBwBuffer()` to free temporary buffers
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
### Directory Structure
* lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n)
@@ -130,7 +130,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
| `HalGPIO` | `InputManager` | Button input handling | *(none)* |
| `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` |
**Location**: [lib/hal/](lib/hal/)
**Location**: [lib/hal/](../lib/hal/)
**Why HAL?**
- Provides consistent error logging per module
@@ -247,7 +247,7 @@ When a template is necessary, limit instantiations: use explicit template instan
### Error Handling Philosophy
**Source**: [src/main.cpp:132-143](src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](lib/GfxRenderer/GfxRenderer.cpp)
**Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp)
**Pattern Hierarchy**:
1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;`
@@ -259,7 +259,7 @@ When a template is necessary, limit instantiations: use explicit template instan
### Acceptable malloc/free Patterns
**Source**: [src/activities/home/HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp)
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
Despite "prefer stack allocation," malloc is acceptable for:
1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
@@ -290,10 +290,10 @@ buffer = nullptr;
- **Document size**: Comment why stack allocation was rejected
**Examples in codebase**:
- Cover image buffers: [HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp#L166)
- Text chunk buffers: [TxtReaderActivity.cpp:259](src/activities/reader/TxtReaderActivity.cpp#L259)
- Bitmap rendering: [GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp#L439-L440)
- OTA update buffer: [OtaUpdater.cpp:40](src/network/OtaUpdater.cpp#L40)
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
---
@@ -305,7 +305,7 @@ buffer = nullptr;
### Logical Button Mapping
**Source**: [src/MappedInputManager.cpp:20-55](src/MappedInputManager.cpp)
**Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp)
Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation.
@@ -352,7 +352,7 @@ Constraint: Physical button positions are fixed on hardware, but their logical f
### Activity Lifecycle and Memory Management
**Source**: [src/main.cpp:132-143](src/main.cpp)
**Source**: [src/main.cpp:132-143](../src/main.cpp)
**CRITICAL**: Activities are **heap-allocated** and **deleted on exit**.
@@ -389,7 +389,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
### FreeRTOS Task Guidelines
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](src/activities/util/KeyboardEntryActivity.cpp)
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp)
**Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)`
@@ -402,7 +402,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
### Global Font Loading
**Source**: [src/main.cpp:40-115](src/main.cpp)
**Source**: [src/main.cpp:40-115](../src/main.cpp)
**All fonts are loaded as global static objects** at firmware startup:
- Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
@@ -423,7 +423,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
- Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`)
- Font rendering data cached in **DRAM** when first used
- `OMIT_FONTS` can reduce binary size for minimal builds
- Font IDs defined in [src/fontIds.h](src/fontIds.h)
- Font IDs defined in [src/fontIds.h](../src/fontIds.h)
**Usage**:
```cpp
@@ -517,7 +517,7 @@ clang-format -i src/**/*.cpp src/**/*.h
4. **Corrupt Cache Files**:
- Delete `.crosspoint/` directory on SD card
- Forces clean re-parse of all EPUBs
- Check file format versions in [docs/file-formats.md](docs/file-formats.md)
- Check file format versions in [docs/file-formats.md](../docs/file-formats.md)
5. **Watchdog Timeout**:
- Loop/task blocked for >5 seconds

View File

@@ -39,6 +39,14 @@ usability over "swiss-army-knife" functionality.
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips.
### In-scope — Technically Unsupported
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be

View File

@@ -308,13 +308,30 @@ If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only fo
### 3.7 Sleep Screen
You can customize the sleep screen by placing custom images in specific locations on the SD card:
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
| Mode | Behavior |
|------|----------|
| **Dark** (default) | The CrossPoint logo on a dark background. |
| **Light** | The CrossPoint logo on a white background. |
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
| **None** | A blank screen. |
> [!NOTE]
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
#### Cover settings
When using **Cover** or **Cover + Custom**, two additional settings apply:
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
#### Custom images
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
> [!TIP]
> For best results:

View File

@@ -0,0 +1,21 @@
# Project Documentation for CrossPoint Reader Firmware
**Date:** 2026-02-09
## Task
Create three documentation files in `/mod/docs/` covering the Xteink X4 hardware capabilities, project file structure, and CI/build/code style for the CrossPoint Reader firmware.
## Changes Made
### New Files
1. **`mod/docs/hardware.md`** -- Device hardware capabilities documentation covering CPU (ESP32-C3), 16MB flash with partition layout, 480x800 eink display (2-bit grayscale, 3 refresh modes), 7 physical buttons, SD card storage, battery monitoring, WiFi networking, USB-C, and power management (deep sleep with GPIO wakeup).
2. **`mod/docs/file-structure.md`** -- Complete project file structure map documenting `src/` (main app with Activity-based UI system), `lib/` (16 libraries including EPUB engine, HAL, fonts, XML/ZIP/JPEG support), `open-x4-sdk/` (hardware driver submodule), `scripts/` (build helpers), `test/` (hyphenation tests), `docs/` (upstream docs), and `.github/` (CI/templates).
3. **`mod/docs/ci-build-and-code-style.md`** -- CI pipeline documentation covering 4 GitHub Actions workflows (CI with format/cppcheck/build/gate jobs, release, RC, PR title check), PlatformIO build system with 3 environments, clang-format 21 rules (2-space indent, 120-col limit, K&R braces), cppcheck static analysis config, and contribution guidelines (semantic PR titles, AI disclosure, scope alignment).
## Follow-up Items
- None. All three documents are complete and ready for review.

View File

@@ -0,0 +1,59 @@
# Sleep Screen Tweaks Implementation
## Task Description
Implemented the two "Sleep Screen tweaks" from the plan:
1. **Gradient fill for letterboxed areas** - When a sleep screen image doesn't match the display's aspect ratio, the void (letterbox) areas are now filled with a dithered gradient sampled from the nearest ~20 pixels of the image's edge, fading toward white.
2. **Fix "Fit" mode for small images** - Images smaller than the 480x800 display are now scaled up (nearest-neighbor) to fit while preserving aspect ratio, instead of being displayed at native size with wasted screen space.
## Changes Made
### `lib/GfxRenderer/GfxRenderer.cpp`
- Modified `drawBitmap()` scaling logic: when both `maxWidth` and `maxHeight` are provided, always computes an aspect-ratio-preserving scale factor (supports both upscaling and downscaling)
- Modified `drawBitmap()` rendering loop: uses block-fill approach where each source pixel maps to a screen rectangle (handles both upscaling blocks and 1:1/downscaling single pixels via a unified loop)
- Applied same changes to `drawBitmap1Bit()` for 1-bit bitmap consistency
- Added `drawPixelGray()` method: draws a pixel using its 2-bit grayscale value, dispatching correctly based on the current render mode (BW, GRAYSCALE_LSB, GRAYSCALE_MSB)
### `lib/GfxRenderer/GfxRenderer.h`
- Added `drawPixelGray(int x, int y, uint8_t val2bit)` declaration
### `lib/GfxRenderer/BitmapHelpers.cpp`
- Added `quantizeNoiseDither()`: hash-based noise dithering that always uses noise (unlike `quantize()` which is controlled by a compile-time flag), used for smooth gradient rendering on the 4-level display
### `lib/GfxRenderer/BitmapHelpers.h`
- Added `quantizeNoiseDither()` declaration
### `src/activities/boot_sleep/SleepActivity.cpp`
- Removed the `if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight)` gate in `renderBitmapSleepScreen()` — position/scale is now always computed regardless of whether the image is larger or smaller than the screen
- Added anonymous namespace with:
- `LetterboxGradientData` struct for edge sample storage
- `sampleBitmapEdges()`: reads bitmap row-by-row to sample per-column (or per-row) average gray values from the first/last 20 pixels of the image edges
- `drawLetterboxGradients()`: draws dithered gradients in letterbox areas using sampled edge colors, interpolating toward white
- Integrated gradient rendering into the sleep screen flow: edge sampling (first pass), then gradient + bitmap rendering in BW pass, and again in each grayscale pass (LSB, MSB)
## Follow-up: Letterbox Fill Settings
Added three letterbox fill options and two new persisted settings:
### `src/CrossPointSettings.h`
- Added `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE` (plain white), `LETTERBOX_GRADIENT` (default), `LETTERBOX_SOLID`
- Added `SLEEP_SCREEN_GRADIENT_DIR` enum: `GRADIENT_TO_WHITE` (default), `GRADIENT_TO_BLACK`
- Added `sleepScreenLetterboxFill` and `sleepScreenGradientDir` member fields
### `src/CrossPointSettings.cpp`
- Incremented `SETTINGS_COUNT` to 32
- Added serialization (read/write) for the two new fields at the end for backward compatibility
### `src/SettingsList.h`
- Added "Letterbox Fill" menu entry (None / Gradient / Solid) in Display category
- Added "Gradient Direction" menu entry (To White / To Black) in Display category
### `src/activities/boot_sleep/SleepActivity.cpp`
- Renamed `drawLetterboxGradients``drawLetterboxFill` with added `solidFill` and `targetColor` parameters
- Solid mode: uses edge color directly (no distance-based interpolation), quantized with noise dithering
- Gradient direction: interpolates from edge color toward `targetColor` (255 for white, 0 for black)
- `renderBitmapSleepScreen` reads the settings and skips edge sampling entirely when fill mode is "None"
## Follow-up Items
- Test with various cover image sizes and aspect ratios on actual hardware
- Test custom images from `/sleep/` directory (1-bit and multi-bit)
- Monitor RAM usage via serial during gradient rendering

View File

@@ -0,0 +1,25 @@
# Edge Data Caching for Sleep Screen Letterbox Fill
## Task
Cache the letterbox edge-sampling calculations so they are only computed once per cover image (on first sleep) and reused from a binary cache file on subsequent sleeps.
## Changes Made
### `src/activities/boot_sleep/SleepActivity.h`
- Added `#include <string>` and updated `renderBitmapSleepScreen` signature to accept an optional `edgeCachePath` parameter (defaults to empty string for no caching).
### `src/activities/boot_sleep/SleepActivity.cpp`
- Added `#include <Serialization.h>` for binary read/write helpers.
- Added `loadEdgeCache()` function in the anonymous namespace: loads edge data from a binary cache file, validates the cache version and screen dimensions against current values to detect stale data.
- Added `saveEdgeCache()` function: writes edge data (edgeA, edgeB arrays + metadata) to a compact binary file (~10 bytes header + 2 * edgeCount bytes data, typically under 1KB).
- Updated `renderBitmapSleepScreen()`: tries loading from cache before sampling. On cache miss, samples edges from bitmap and saves to cache. On cache hit, skips the bitmap sampling pass entirely.
- Updated `renderCoverSleepScreen()`: derives the edge cache path from the cover BMP path (e.g. `cover.bmp` -> `cover_edges.bin`) and passes it to `renderBitmapSleepScreen`. The cache is stored alongside the cover BMP in the book's `.crosspoint` directory.
- Custom sleep images (`renderCustomSleepScreen`) do not use caching since the image may change randomly each sleep.
### Cache Design
- **Format**: Binary file with version byte, screen dimensions, horizontal flag, edge count, letterbox sizes, and two edge color arrays.
- **Invalidation**: Validated by cache version and screen dimensions. Naturally scoped per-book (lives in `/.crosspoint/epub_<hash>/`). Different cover modes (FIT vs CROP) produce different BMP files and thus different cache paths.
- **Size**: ~970 bytes for a 480-wide image.
## Follow-up Items
- None

View File

@@ -0,0 +1,26 @@
# Letterbox Fill: 4-Mode Restructure (Solid, Blended, Gradient, None)
## Task
Restructure the letterbox fill modes from 3 (None, Gradient, Solid) to 4 distinct modes with clearer semantics.
## New Modes
1. **Solid** (new) - Picks the dominant (average) shade from the edge and fills the entire letterbox area with that single dithered color.
2. **Blended** (renamed from old "Solid") - Uses per-pixel sampled edge colors with noise dithering, no distance-based interpolation.
3. **Gradient** - Existing gradient behavior (interpolates per-pixel edge color toward a target color).
4. **None** - No fill.
## Changes Made
### `src/CrossPointSettings.h`
- Updated `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_BLENDED=2`, `LETTERBOX_GRADIENT=3`.
- Note: enum values changed from the old 3-value layout. Existing saved settings may need reconfiguring.
### `src/SettingsList.h`
- Updated "Letterbox Fill" option labels to: "None", "Solid", "Blended", "Gradient".
### `src/activities/boot_sleep/SleepActivity.cpp`
- `drawLetterboxFill()`: Changed signature from `bool solidFill` to `uint8_t fillMode`. Added dominant shade computation for SOLID mode (averages all edge samples into one value per side). BLENDED and SOLID both skip gradient interpolation; SOLID additionally skips per-pixel edge lookups.
- `renderBitmapSleepScreen()`: Removed `solidFill` bool, passes `fillMode` directly to `drawLetterboxFill`. Updated log message to show the mode name.
## Follow-up Items
- None

View File

@@ -0,0 +1,46 @@
# Dictionary Feature Bug Fixes
**Date:** 2026-02-12
**Branch:** mod/add-dictionary
## Task
Fix three bugs reported after initial implementation of PR #857 dictionary word lookup feature.
## Changes Made
### 1. Fix: Lookup fails after first successful lookup (Dictionary.cpp)
**Root cause:** `cleanWord()` lowercases the search term, but the dictionary index is sorted case-sensitively (uppercase entries sort before lowercase). The binary search lands in the wrong segment on the second pass because the index is already loaded and the sparse offset table was built for a case-sensitive sort order.
**Fix:** Extracted the binary-search + linear-scan logic into a new `searchIndex()` private helper. The `lookup()` method now performs a **two-pass search**: first with the lowercased word, then with the first-letter-capitalized variant. This handles dictionaries that store headwords as "Hello" instead of "hello". Also removed `stripHtml` from the header since HTML is now rendered, not stripped.
**Files:** `src/util/Dictionary.h`, `src/util/Dictionary.cpp`
### 2. Fix: Raw HTML displayed in definitions (DictionaryDefinitionActivity)
**Root cause:** Dictionary uses `sametypesequence=h` (HTML format). The original activity rendered definitions as plain text.
**Fix:** Complete rewrite of the definition activity to **render HTML with styled text**:
- New `parseHtml()` method tokenizes HTML into `TextAtom` structs (word + style + newline directives)
- Supports bold (`<b>`, `<strong>`, headings), italic (`<i>`, `<em>`), and mixed bold-italic via `EpdFontFamily::Style`
- Handles `<ol>` (numbered with digit/alpha support), `<ul>` (bullet points), nested lists with indentation
- Decodes HTML entities (named + numeric/hex → UTF-8)
- Skips `<svg>` content, treats `</html>` as section separators
- `wrapText()` flows styled atoms into positioned line segments (`Segment` struct with x-offset and style)
- `renderScreen()` draws each segment with correct position and style via `renderer.drawText(fontId, x, y, text, true, style)`
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
### 3. Fix: No button hints on word selection screen (DictionaryWordSelectActivity)
**Fix:** Added `GUI.drawButtonHints()` call at the end of `renderScreen()` with labels: "« Back", "✓ Lookup", "↕ Row", "↔ Word".
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
## Follow-up Items
- If the reader font doesn't include bold/italic variants, styled text gracefully falls back to regular style
- Nested list indentation uses 15px per level after the first
- Alpha list numbering (`list-style-type: lower-alpha`) is supported; other custom list styles fall back to numeric
- Button hint labels may need tuning once tested on device (especially in landscape orientation)

View File

@@ -0,0 +1,53 @@
# Dictionary Feature Bug Fixes (Round 2)
**Date:** 2026-02-12
**Branch:** mod/add-dictionary
## Task
Fix three remaining bugs with the dictionary word lookup feature after initial implementation and first round of fixes.
## Changes Made
### 1. Fix: Lookup only works for first word searched (Dictionary.cpp)
**Root cause:** The binary search used C++ `operator<=` (case-sensitive, pure `strcmp` order) for comparing index entries, but StarDict `.idx` files are sorted using `stardict_strcmp` — a two-level comparison that sorts case-*insensitively* first, then uses case-sensitive `strcmp` as a tiebreaker. This means entries like "Simple" and "simple" are adjacent, and uppercase/lowercase entries for the same letter are interleaved (e.g., "Silver", "silver", "Simple", "simple", "Simpson", "simpson").
With the wrong sort order, the binary search overshoots for many words: uppercase entries like "Simpson" are case-sensitively < "simple" (because 'S' < 's'), so `lo` moves past the segment actually containing "simple". The linear scan then starts too late and doesn't find the word. By coincidence, some words (like "professor") happen to land in correct segments while others (like "simple") don't.
**Fix:**
- Added `stardictCmp()` (case-insensitive first, then case-sensitive tiebreaker) and `asciiCaseCmp()` helper functions
- Binary search now uses `stardictCmp(key, word) <= 0` instead of `key <= word`
- Linear scan early termination now uses `stardictCmp(key, word) > 0` instead of `key > word`
- Exact match now uses `asciiCaseCmp(key, word) == 0` (case-insensitive) since `cleanWord` lowercases the search term but the dictionary may store entries in any case
- Removed the two-pass search approach (no longer needed — single pass handles all casing)
**Files:** `src/util/Dictionary.cpp`
### 2. Fix: Unrendered glyphs in pronunciation guide (DictionaryDefinitionActivity.cpp)
**Root cause:** The dictionary stores IPA pronunciation as raw text between entries, e.g., `/əmˈsɛlvz/` appearing between `</html>` and the next `<p>` tag. These IPA Extension Unicode characters (U+0250U+02FF) are not in the e-ink display's bitmap font, rendering as empty boxes.
**Fix:** Added IPA detection in `parseHtml()`: when encountering `/` or `[` delimiters, the parser looks ahead for a matching close delimiter within 80 characters. If the enclosed content contains any non-ASCII byte (> 0x7F), the entire section (including delimiters) is skipped. This removes IPA transcriptions like `/ˈsɪmpəl/` and `[hɜː(ɹ)b]` while preserving legitimate ASCII bracket content like "[citation needed]" or "and/or".
**Files:** `src/activities/reader/DictionaryDefinitionActivity.cpp`
### 3. Fix: Thinner button hints with overlap detection (DictionaryWordSelectActivity)
**Root cause:** Button hints used `GUI.drawButtonHints()` which draws 40px-tall buttons, taking excessive space over the book page content. No overlap detection meant hints could obscure the selected word at the bottom of the screen.
**Fix:**
- Replaced `GUI.drawButtonHints()` with a custom `drawHints()` method
- Draws thinner hints (22px instead of 40px) using `drawRect` + small text
- Converts the selected word's bounding box from the current orientation to portrait coordinates (handles portrait, inverted, landscape CW/CCW)
- Checks vertical and horizontal overlap between each of the 4 button hint areas and the selected word (including hyphenation continuations)
- Individual hints that overlap the cursor are hidden (white area cleared, no button drawn)
- Uses the theme's button positions `[58, 146, 254, 342]` to match the physical button layout
**Files:** `src/activities/reader/DictionaryWordSelectActivity.h`, `src/activities/reader/DictionaryWordSelectActivity.cpp`
## Follow-up Items
- The landscape coordinate conversions for overlap detection are best-guess transforms; if they're wrong for the specific device rotation mapping, they may need tuning after testing
- The IPA skip heuristic is conservative (only skips content with non-ASCII in `/`/`[` delimiters); some edge-case IPA content outside these delimiters would still show
- `SMALL_FONT_ID` is now included via `fontIds.h` in the word select activity

View File

@@ -0,0 +1,47 @@
# Dictionary Feature Bug Fixes (Round 3)
**Date:** 2026-02-12
**Branch:** mod/add-dictionary
## Task
Fix three issues reported after round 2 of dictionary fixes.
## Changes Made
### 1. Fix: Definitions truncated for some words (Dictionary.cpp)
**Root cause:** The `asciiCaseCmp` case-insensitive match introduced in round 2 returns the *first* case variant found in the index. In StarDict order, "Professor" (capitalized) sorts before "professor" (lowercase). If the dictionary has separate entries for each — e.g., "Professor" as a title (short definition) and "professor" as the common noun (full multi-page definition) — the shorter entry is returned.
**Fix:** The linear scan in `searchIndex` now remembers the first case-insensitive match as a fallback, but continues scanning adjacent entries (case variants are always adjacent in StarDict order). If an exact case-sensitive match is found, it's used immediately. Otherwise, the first case-insensitive match is used. This ensures `cleanWord("professor")``"professor"` finds the full lowercase entry, not the shorter capitalized one.
**Files:** `src/util/Dictionary.cpp`
### 2. Fix: Non-renderable foreign script characters in definitions (DictionaryDefinitionActivity)
**Root cause:** Dictionary definitions include text from other languages (Chinese, Greek, Arabic, Cyrillic, etc.) as etymological references or examples. These characters aren't in the e-ink bitmap font and render as empty boxes. This is the same class of issue as the IPA pronunciation fix from round 2, but affecting inline content within definitions.
**Fix:**
- Added `isRenderableCodepoint(uint32_t cp)` static helper that whitelists character ranges the e-ink font supports:
- U+0000U+024F: Basic Latin through Latin Extended-B (ASCII + accented chars)
- U+0300U+036F: Combining Diacritical Marks
- U+2000U+206F: General Punctuation (dashes, quotes, bullets, ellipsis)
- U+20A0U+20CF: Currency Symbols
- U+2100U+214F: Letterlike Symbols
- U+2190U+21FF: Arrows
- Replaced the byte-by-byte character append in `parseHtml()` with a UTF-8-aware decoder that reads multi-byte sequences, decodes the codepoint, and only appends renderable characters. Invalid or non-renderable characters are silently skipped.
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
### 3. Fix: Revert to standard-height hints, keep overlap hiding (DictionaryWordSelectActivity)
**What changed:** Reverted from 22px thin custom hints back to the standard 40px theme-style buttons (rounded corners with `cornerRadius=6`, `SMALL_FONT_ID` text, matching `LyraTheme::drawButtonHints` exactly). The overlap detection is preserved.
**Key design choice:** Instead of calling `GUI.drawButtonHints()` (which always clears all 4 button areas, erasing page content even for hidden buttons), the method draws each button individually in portrait mode. Hidden buttons are skipped entirely (`continue`), so the page content and word highlight underneath remain visible. Non-hidden buttons get the full theme treatment: white fill + rounded rect border + centered text.
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
## Follow-up Items
- The `isRenderableCodepoint` whitelist is conservative — if the font gains additional glyph coverage (e.g., Greek letters for math), the whitelist can be extended
- Entity-decoded characters bypass the codepoint filter since they're appended as raw bytes; this is fine for the current entity set (all produce ASCII or General Punctuation characters)

View File

@@ -0,0 +1,40 @@
# Bookmark Feature Implementation
## Task
Implement bookmark functionality for the e-reader, replacing existing "Coming soon" stubs with full add/remove bookmark, visual page indicator, and bookmark navigation features.
## Changes Made
### New Files Created
- **`src/util/BookmarkStore.h`** / **`src/util/BookmarkStore.cpp`** - Bookmark persistence utility. Stores bookmarks as binary data (`bookmarks.bin`) per-book in the epub cache directory on SD card. Each bookmark is a (spineIndex, pageNumber) pair (4 bytes). Provides static methods: `load`, `save`, `addBookmark`, `removeBookmark`, `hasBookmark`.
- **`src/activities/reader/EpubReaderBookmarkSelectionActivity.h`** / **`.cpp`** - New activity for the "Go to Bookmark" list UI, modeled on the existing chapter selection activity. Shows bookmark entries as "Chapter Title - Page N" with ButtonNavigator for scrolling. Selecting a bookmark navigates to that spine/page.
### Edited Files
- **`src/activities/reader/EpubReaderMenuActivity.h`** - Added `REMOVE_BOOKMARK` to `MenuAction` enum. Changed `buildMenuItems()` to accept `isBookmarked` parameter; dynamically shows "Remove Bookmark" or "Add Bookmark" as the first menu item.
- **`src/activities/reader/EpubReaderActivity.cpp`** - Main integration point:
- Added includes for `BookmarkStore.h` and `EpubReaderBookmarkSelectionActivity.h`
- Menu creation now computes `isBookmarked` state and passes it through
- `ADD_BOOKMARK` handler: calls `BookmarkStore::addBookmark()`, shows "Bookmark added" popup
- `REMOVE_BOOKMARK` handler (new): calls `BookmarkStore::removeBookmark()`, shows "Bookmark removed" popup
- `GO_TO_BOOKMARK` handler: loads bookmarks, opens `EpubReaderBookmarkSelectionActivity` if any exist, falls back to Table of Contents if no bookmarks but TOC exists, otherwise returns to reader
- `renderContents()`: draws a small bookmark ribbon (fillPolygon, 5-point shape) in the top-right corner when the current page is bookmarked
## Follow-up Changes (same session)
### Force half refresh on menu exit
- `onReaderMenuBack()`: sets `pagesUntilFullRefresh = 1` so the next render uses `HALF_REFRESH` to clear menu/popup ghosting artifacts from the e-ink display.
- `ADD_BOOKMARK` / `REMOVE_BOOKMARK` handlers: also set `pagesUntilFullRefresh = 1` after their popups.
### Bookmark snippet (first sentence)
- `Bookmark` struct now includes a `snippet` string field storing the first sentence from the bookmarked page.
- `BookmarkStore` binary format upgraded to v2: version marker byte (0xFF) + count + entries with variable-length snippet. Backward-compatible: reads v1 files (no snippets) gracefully.
- `addBookmark()` now accepts an optional `snippet` parameter (max 120 chars).
- `EpubReaderActivity::onReaderMenuConfirm(ADD_BOOKMARK)`: extracts the first sentence from the page by iterating PageLine elements and their TextBlock words, stopping at sentence-ending punctuation (.!?:).
- `EpubReaderBookmarkSelectionActivity::getBookmarkLabel()`: displays bookmark as "Chapter Title - First sentence here - Page N".
## Follow-up Items
- Test on device to verify bookmark ribbon sizing/positioning looks good across orientations
- Consider caching bookmark state in memory to avoid SD reads on every page render (currently `hasBookmark` reads from SD each time in `renderContents`)
- The bookmark selection list could potentially support deleting bookmarks directly from the list in a future iteration

View File

@@ -0,0 +1,32 @@
# Implement Dictionary Word Lookup Feature (PR #857)
## Task
Ported upstream PR #857 (dictionary word lookup feature) into the local codebase on `mod/add-dictionary` branch. Two adaptations were made:
1. **Vector compatibility**: PR #857 was written against upstream `master` which used `std::list` for word storage. Our codebase already has PR #802 applied (list-to-vector). The `TextBlock.h` getters added by PR #857 were changed to return `const std::vector<...>&` instead of `const std::list<...>&`.
2. **Dictionary path tweak**: Changed dictionary file lookup from SD card root (`/dictionary.idx`, `/dictionary.dict`) to a `/.dictionary/` subfolder (`/.dictionary/dictionary.idx`, `/.dictionary/dictionary.dict`).
## Changes Made
### New files (10)
- `src/util/Dictionary.h` / `.cpp` -- StarDict 3 format dictionary lookup (sparse index + binary search)
- `src/util/LookupHistory.h` / `.cpp` -- Per-book lookup history stored in book cache dir
- `src/activities/reader/DictionaryDefinitionActivity.h` / `.cpp` -- Definition display with pagination
- `src/activities/reader/DictionaryWordSelectActivity.h` / `.cpp` -- Word selection from current page with orientation-aware navigation
- `src/activities/reader/LookedUpWordsActivity.h` / `.cpp` -- Lookup history browser with long-press delete
### Modified files (5)
- `lib/Epub/Epub/blocks/TextBlock.h` -- Added `getWords()`, `getWordXpos()`, `getWordStyles()` accessors (returning `std::vector`)
- `lib/Epub/Epub/Page.h` -- Added `getBlock()` accessor to `PageLine`
- `src/activities/reader/EpubReaderMenuActivity.h` -- Added `LOOKUP`/`LOOKED_UP_WORDS` enum values, `hasDictionary` constructor param, dynamic `buildMenuItems()`
- `src/activities/reader/EpubReaderActivity.h` -- Added includes for new activity headers
- `src/activities/reader/EpubReaderActivity.cpp` -- Added includes, `Dictionary::exists()` check, `LOOKUP` and `LOOKED_UP_WORDS` case handling
## Follow-up Items
- Dictionary files (`dictionary.idx`, `dictionary.dict`, `dictionary.ifo`) must be placed in `/.dictionary/` folder on the SD card root
- Menu items "Lookup" and "Lookup History" only appear when dictionary files are detected

View File

@@ -0,0 +1,56 @@
# Dictionary Feature Polish & Menu Reorganization
**Date:** 2026-02-12
## Task Description
Continued polishing the dictionary word lookup feature across multiple iterations: fixing side button hint placement and orientation, adding CCW text rotation, fixing pronunciation line rendering, adding index caching, and reorganizing the reader menu.
## Changes Made
### Side button hint fixes (`DictionaryWordSelectActivity.cpp`)
- Moved `drawSideButtonHints` call inside `drawHints()` where renderer is in portrait mode (fixes wrong placement)
- Made all button hint labels orientation-aware (portrait, inverted, landscape CW/CCW each get correct labels matching their button-to-action mapping)
- Replaced `GUI.drawSideButtonHints()` with custom drawing: solid background, overlap/cursor hiding, text truncation, and orientation-aware rotation
- Changed word navigation labels from "Prev Word"/"Next Word" to "« Word"/"Word »"
### GfxRenderer CCW text rotation (`lib/GfxRenderer/`)
- Added `drawTextRotated90CCW()` to `GfxRenderer.h` and `GfxRenderer.cpp`
- Mirrors the existing CW rotation: text reads top-to-bottom instead of bottom-to-top
- Used for side button hints in landscape CCW orientation
### Definition screen fixes (`DictionaryDefinitionActivity.cpp/.h`)
- Fixed pronunciation commas: definitions start with `/ˈsɪm.pəl/, /ˈsɪmpəl/` before `<p>` — now skips all content before first `<` tag in `parseHtml()`
- Added side button hints with proper CCW rotation and solid backgrounds
- Updated bottom button labels: "« Back", "" (hidden stub), "« Page", "Page »"
- Added half refresh on initial screen entry (`firstRender` flag)
### Dictionary index caching (`Dictionary.h/.cpp`)
- New `loadCachedIndex()`: reads `/.dictionary/dictionary.cache` — validates magic + idx file size, loads sparse offsets directly (~7KB binary read vs 17MB scan)
- New `saveCachedIndex()`: persists after first full scan
- Cache format: `[magic 4B][idxFileSize 4B][totalWords 4B][count 4B][offsets N×4B]`
- Auto-invalidates when `.idx` file size changes
- New public methods: `cacheExists()`, `deleteCache()`
### Reader menu reorganization (`EpubReaderMenuActivity.h`, `EpubReaderActivity.cpp`)
- New `MenuAction` enum values: `ADD_BOOKMARK`, `GO_TO_BOOKMARK`, `DELETE_DICT_CACHE`
- Reordered menu: Add Bookmark, Lookup Word, Lookup Word History, Reading Orientation, Table of Contents, Go to Bookmark, Go to %, Close Book, Sync Progress, Delete Book Cache, Delete Dictionary Cache
- Renamed: "Go to Chapter" → "Table of Contents", "Go Home" → "Close Book", "Lookup" → "Lookup Word"
- Bookmark stubs show "Coming soon" popup
- Delete Dictionary Cache checks existence and clears in-memory state
## Files Modified
- `lib/GfxRenderer/GfxRenderer.h` — added `drawTextRotated90CCW` declaration
- `lib/GfxRenderer/GfxRenderer.cpp` — added `drawTextRotated90CCW` implementation
- `src/util/Dictionary.h` — added `cacheExists()`, `deleteCache()`, `loadCachedIndex()`, `saveCachedIndex()`
- `src/util/Dictionary.cpp` — cache load/save implementation, delete cache
- `src/activities/reader/DictionaryWordSelectActivity.cpp` — orientation-aware hints, custom side button drawing
- `src/activities/reader/DictionaryDefinitionActivity.h` — added `firstRender` flag
- `src/activities/reader/DictionaryDefinitionActivity.cpp` — pronunciation fix, side hints, half refresh, label changes
- `src/activities/reader/EpubReaderMenuActivity.h` — new enum values, reordered menu
- `src/activities/reader/EpubReaderActivity.cpp` — handlers for new menu actions
## Follow-up Items
- Bookmark feature implementation (stubs are in place)
- Test CCW text rotation rendering on device
- Verify cache invalidation works when dictionary files are replaced

View File

@@ -0,0 +1,28 @@
# Implement Letterbox Edge Row Copy for MATCHED Mode
## Task
Implement the "FrameBuffer Edge Row Copy" plan for the MATCHED letterbox fill mode. Instead of computing letterbox colors from sampled edge data, the new approach copies the cover's rendered edge row directly from the frameBuffer into the letterbox area after each `drawBitmap` call.
## Changes Made
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Added `#include <cstring>`** for `memcpy` usage.
- **Added `copyEdgeRowsToLetterbox()` helper** (anonymous namespace): Copies physical columns (horizontal letterbox) or physical rows (vertical letterbox) in the frameBuffer. For horizontal letterbox, iterates per-bit across 480 physical rows. For vertical letterbox, uses `memcpy` of 100-byte physical rows.
- **Updated `renderBitmapSleepScreen()`**:
- Added `scaledWidth`/`scaledHeight` computation matching `drawBitmap`'s floor logic.
- Added `isMatched` flag.
- MATCHED mode now skips edge sampling entirely (`sampleBitmapEdges` / cache load).
- After each `drawBitmap` call (BW, LSB, MSB passes), calls `copyEdgeRowsToLetterbox` for MATCHED mode.
- **Cleaned up dead code**:
- Removed the entire MATCHED case from `drawLetterboxFill()` (no longer called for MATCHED).
- Removed `grayToVal2bit` helper (was only used by the removed MATCHED case).
- Removed `skipFillInGreyscale` flag (no longer needed — the edge copy participates in all passes naturally).
## Build
Successfully compiled with `pio run` (0 errors, 0 warnings relevant to changes).
## Follow-up
- Needs on-device testing to verify:
1. The letterbox blends seamlessly with the cover edge (pixel-perfect 1:1 match).
2. No scan coupling corruption (the scattered pixel distribution from dithering should cause less coupling than uniform blocks).
3. If corruption is still unacceptable, the fallback is the previous flat-fill + greyscale-skip approach (revert this change).

View File

@@ -0,0 +1,41 @@
# Merge master into mod/master
**Date:** 2026-02-12
## Task
Compare upstream `master` (14 new commits) with `mod/master` (2 mod commits) since their common ancestor (`6202bfd` — release/1.0.0 merge), assess merge risk, and perform the merge.
## Branch Summary
### Upstream (`master`) — 14 commits, 47 files, ~6000 lines
- Unified navigation handling with ButtonNavigator utility
- Italian hyphenation support
- Natural sort in file browser
- Auto WiFi reconnect to last network
- Extended Python debugging monitor
- More power saving on idle
- OPDS fixes (absolute URLs, prevent sleep during download)
- Uniform debug message formatting (millis() timestamps)
- File browser Back/Home label fix, GPIO trigger fix
- USER_GUIDE.md updates
### Mod (`mod/master`) — 2 commits, 10 files, ~588 lines
- `.gitignore` tweaks for mod fork
- Sleep screen letterbox fill and image upscaling feature
## Conflict Resolution
Single conflict in `src/activities/boot_sleep/SleepActivity.cpp`:
- **Upstream** changed `Serial.println``Serial.printf("[%lu] [SLP] ...\n", millis())` for uniform debug logging
- **Mod** had already adopted this format in new code, but the original lines it modified were the old format
- **Resolution:** Kept mod's `renderBitmapSleepScreen(bitmap, edgeCachePath)` call with upstream's `millis()` log format
## Result
Merge commit: `182c236`
## Follow-up
- Test sleep screen behavior end-to-end (letterbox fill + upstream idle power saving changes)
- Verify new upstream features (navigation, WiFi auto-connect) work alongside mod changes

View File

@@ -0,0 +1,23 @@
# Add `env:mod` with version + git hash
## Task
Add a PlatformIO environment that flashes firmware with a `-mod+<git_hash>` version suffix (e.g. `1.0.0-mod+a3f7c21`).
## Changes
### New file: `scripts/inject_mod_version.py`
- PlatformIO pre-build script
- Reads `version` from the `[crosspoint]` section of `platformio.ini`
- Runs `git rev-parse --short HEAD` to get the current commit hash
- Injects `-DCROSSPOINT_VERSION="{version}-mod+{hash}"` into build flags
### Modified: `platformio.ini`
- Added `[env:mod]` section (lines 58-64) that extends `base`, includes the new script via `extra_scripts`, and inherits base build flags
## Usage
```
pio run -e mod -t upload
```
## Follow-up
- None

View File

@@ -0,0 +1,39 @@
# Prerender Book Covers/Thumbnails on First Open
**Date:** 2026-02-12
**Branch:** `mod/prerender-book-covers`
## Task
Implement todo item: "Process/render all covers/thumbs when opening book for first time" with a progress indicator so the reader doesn't appear frozen.
## Changes Made
### `src/components/UITheme.h`
- Added `PRERENDER_THUMB_HEIGHTS[]` and `PRERENDER_THUMB_HEIGHTS_COUNT` constants (226, 400) representing all known theme `homeCoverHeight` values (Lyra and Base). This ensures thumbnails are pre-generated for all themes.
### `src/activities/reader/EpubReaderActivity.cpp`
- Added prerender block in `onEnter()` after `setupCacheDir()` and before `addBook()`.
- Checks whether `cover.bmp`, `cover_crop.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` exist.
- If any are missing, shows "Preparing book..." popup with a progress bar that updates after each generation step.
- On subsequent opens, all files already exist and the popup is skipped entirely.
### `src/activities/reader/XtcReaderActivity.cpp`
- Added same prerender pattern in `onEnter()`.
- Generates `cover.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` (XTC has no cropped cover variant).
### `src/activities/reader/TxtReaderActivity.cpp`
- Added prerender for `cover.bmp` only (TXT has no thumbnail support).
- Shows "Preparing book..." popup if the cover needs generating.
## Design Decisions
- **Letterbox edge data not prerendered:** The sleep screen's letterbox gradient fill (`cover_edges.bin`) depends on runtime settings (crop mode, screen dimensions) and is already efficiently cached after first sleep. The expensive part (JPEG-to-BMP conversion) is what this change addresses.
- **TXT `addBook()` cover path unchanged:** The `coverBmpPath` field in `RecentBook` is used for home screen thumbnails, not sleep covers. Since TXT has no thumbnail support, passing `""` remains correct.
- **HomeActivity::loadRecentCovers() kept as fallback:** Books opened before this change will still have thumbnails generated lazily on the Home screen. No code removal needed.
## Follow-up Items
- Mark todo item as complete in `mod/docs/todo.md`
- Test on device with each book format (EPUB, XTC/XTCH, TXT)
- If a new theme is added with a different `homeCoverHeight`, update `PRERENDER_THUMB_HEIGHTS`

View File

@@ -0,0 +1,33 @@
# Letterbox Fill: Hash-Based Block Dithering Fix & Cleanup
**Date:** 2026-02-13
## Task Description
Resolved an e-ink display crosstalk issue where a specific book cover ("The World in a Grain") became completely washed out and invisible when using "Dithered" letterbox fill mode. The root cause was pixel-level Bayer dithering creating a high-frequency checkerboard pattern in the BW pass for gray values in the 171-254 range (level-2/level-3 boundary), which caused display crosstalk during HALF_REFRESH.
## Solution
Hash-based block dithering with 2x2 pixel blocks for gray values in the problematic BW-boundary range (171-254). Each 2x2 block gets a uniform level (2 or 3) determined by a spatial hash, with the proportion approximating the target gray. Standard Bayer dithering is used for all other gray ranges.
## Changes Made
### Modified Files
- **`src/activities/boot_sleep/SleepActivity.cpp`** — Added `bayerCrossesBwBoundary()` and `hashBlockDither()` functions; `drawLetterboxFill()` uses hash-based block dithering for BW-boundary gray values, standard Bayer for everything else. Removed all debug instrumentation (H1-H20 logs, frame buffer checksums, edge histograms, rewind verification).
- **`src/CrossPointSettings.h`** — Reordered letterbox fill enum to `DITHERED=0, SOLID=1, NONE=2` with `DITHERED` as default.
- **`src/SettingsList.h`** — Updated settings UI labels to match new enum order.
- **`lib/GfxRenderer/GfxRenderer.h`** — Removed `getRenderMode()` getter (was only needed by debug instrumentation).
### Deleted Files
- 16 debug log files from `.cursor/` directory
## Key Findings
- Single-pixel alternation (block=1) causes display crosstalk regardless of pattern regularity (Bayer or hash).
- 2x2 minimum pixel runs are sufficient to avoid crosstalk on this e-ink display.
- The irregular hash pattern is less visually noticeable than a regular Bayer grid at the same block size.
- The issue only affects gray values 171-254 where Bayer produces a level-2/level-3 mix (the BW pass boundary).
## Follow-up Items
- None. Feature is stable and working for all tested book covers.

View File

@@ -0,0 +1,49 @@
# Letterbox Fill: Replace Dithering with Edge Replication ("Extend Edges")
**Date:** 2026-02-13
## Task Description
After implementing Bayer ordered dithering for the "Blended" letterbox fill mode (replacing earlier noise dithering), the user reported that the problematic cover (*The World in a Grain* by Vince Beiser) looked even worse. Investigation revealed that **any dithering technique** is fundamentally incompatible with the e-ink multi-pass rendering pipeline for large uniform fill areas:
- The BW HALF_REFRESH pass creates a visible dark/white pattern in the letterbox
- The subsequent greyscale correction can't cleanly handle mixed-level patterns in large uniform areas
- This causes crosstalk artifacts that can affect adjacent cover rendering
- Uniform fills (all same level, like Solid mode) work fine because all pixels go through identical correction
The user's key insight: the display already renders the cover's grayscale correctly (including Atkinson-dithered mixed levels). Instead of re-dithering an averaged color, replicate the cover's actual boundary pixels into the letterbox -- letting the display render them the same way it renders the cover itself.
## Changes Made
### `src/CrossPointSettings.h`
- Renamed `LETTERBOX_BLENDED` (value 2) → `LETTERBOX_EXTENDED`
- Updated comment to reflect "None / Solid / Extend Edges"
### `src/SettingsList.h`
- Changed UI label from `"Blended"``"Extend Edges"`
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Removed**: `BAYER_4X4` matrix, `quantizeBayerDither()` function (all Bayer dithering code)
- **Added**: `getPackedPixel()`, `setPackedPixel()` helpers for packed 2-bit array access
- **Rewrote** `LetterboxFillData` struct:
- Added `edgeA`/`edgeB` (dynamically allocated packed 2-bit arrays) for per-pixel edge data
- Added `edgePixelCount`, `scale` for coordinate mapping
- Added `freeEdgeData()` cleanup method
- **Renamed** `computeEdgeAverages()``computeEdgeData()`:
- New `captureEdgePixels` parameter controls whether to allocate and fill edge arrays
- For horizontal letterboxing: captures first/last visible BMP rows
- For vertical letterboxing: captures leftmost/rightmost pixel per visible row
- Still computes averages for SOLID mode in the same pass
- **Rewrote** `drawLetterboxFill()`:
- SOLID mode: unchanged (uniform fill with snapped level)
- EXTENDED mode: maps screen coordinates → BMP coordinates via scale factor, looks up stored 2-bit pixel values from the cover's boundary row/column
- **Updated** `renderBitmapSleepScreen()`: new log messages, `freeEdgeData()` call at end
## Backward Compatibility
- Enum value 2 is unchanged (was `LETTERBOX_BLENDED`, now `LETTERBOX_EXTENDED`)
- Serialized settings files continue to work without migration
## Follow-up Items
- Test "Extend Edges" mode with both covers (Power Broker and World in a Grain)
- Test vertical letterboxing (left/right) if applicable covers are available
- Verify SOLID mode still works as expected

View File

@@ -0,0 +1,42 @@
# Letterbox Fill Redesign
**Date:** 2026-02-13
**Task:** Strip out the 5-mode letterbox edge fill system and replace with a simplified 3-mode design
## Changes Made
### Problem
The existing letterbox fill feature had 5 modes (None, Solid, Blended, Gradient, Matched) with ~300 lines of complex code including per-pixel edge arrays, malloc'd buffers, binary edge caching, framebuffer-level column/row copying, and a gradient direction sub-setting. Several modes introduced visual corruption that couldn't be resolved.
### New Design
Simplified to 3 modes:
- **None** -- no fill
- **Solid** -- computes dominant edge color, snaps to nearest of 4 e-ink levels (black/dark gray/light gray/white), fills uniformly
- **Blended** -- computes dominant edge color, fills with exact gray value using noise dithering for smooth approximation
### Files Changed
1. **`src/CrossPointSettings.h`** -- Removed `LETTERBOX_GRADIENT`, `LETTERBOX_MATCHED` enum values; removed `SLEEP_SCREEN_GRADIENT_DIR` enum and `sleepScreenGradientDir` member; changed default to `LETTERBOX_NONE`
2. **`src/SettingsList.h`** -- Trimmed Letterbox Fill options to `{None, Solid, Blended}`; removed Gradient Direction setting entry
3. **`src/CrossPointSettings.cpp`** -- Removed `sleepScreenGradientDir` from write path; added dummy read for backward compatibility with old settings files; decremented `SETTINGS_COUNT` from 32 to 31
4. **`src/activities/boot_sleep/SleepActivity.cpp`** -- Major rewrite:
- Removed: `LetterboxGradientData` struct, `loadEdgeCache()`/`saveEdgeCache()`, `sampleBitmapEdges()`, `copyEdgeRowsToLetterbox()`, old `drawLetterboxFill()`
- Added: `LetterboxFillData` struct (2 bytes vs arrays), `snapToEinkLevel()`, `computeEdgeAverages()` (running sums only, no malloc), simplified `drawLetterboxFill()`
- Cleaned `renderBitmapSleepScreen()`: removed matched/gradient logic, edge cache paths, unused `scaledWidth`/`scaledHeight`
- Cleaned `renderCoverSleepScreen()`: removed edge cache path derivation
- Removed unused includes (`<Serialization.h>`, `<cstring>`), added `<cmath>`
5. **`src/activities/boot_sleep/SleepActivity.h`** -- Removed `edgeCachePath` parameter from `renderBitmapSleepScreen()` signature; removed unused `<string>` include
### Backward Compatibility
- Enum values 0/1/2 (None/Solid/Blended) unchanged -- existing settings preserved
- Old Gradient (3) or Matched (4) values rejected by `readAndValidate`, falling back to default (None)
- Old `sleepScreenGradientDir` byte consumed via dummy read during settings load
- Orphaned `_edges.bin` cache files on SD cards are harmless
## Follow-up Items
- Test all 3 fill modes on device with various cover aspect ratios
- Consider cleaning up orphaned `_edges.bin` files (optional, low priority)

View File

@@ -0,0 +1,41 @@
# Merge upstream/master into mod/master
**Date:** 2026-02-13
**Branch:** `mod/merge-upstream` (from `mod/master`)
**Commit:** `82bfbd8`
## Task
Merged 3 new upstream/master commits into the mod fork:
1. `7a385d7` feat: Allow screenshot retrieval from device (#820)
2. `cb24947` feat: Add central logging pragma (#843) — replaces Serial.printf with LOG_* macros across ~50 files, adds Logging library
3. `6e51afb` fix: Account for nbsp character as non-breaking space (#757)
## Conflicts Resolved
### src/main.cpp (1 conflict region)
- Kept mod's `HalPowerManager` (deep sleep, power saving) + upstream's `Logging.h` and `LOG_*` macros + screenshot serial handler (`logSerial`)
### src/activities/boot_sleep/SleepActivity.cpp (3 conflict regions)
- Kept mod's entire letterbox fill rework (~330 lines of dithering, edge caching, etc.)
- Replaced upstream's reverted positioning logic (size-gated) with mod's always-compute-scale approach
- Applied upstream's `LOG_*` pattern to all mod `Serial.printf` calls
## Additional Changes (beyond conflict resolution)
- **lib/GfxRenderer/GfxRenderer.cpp** — Fixed one `Serial.printf` that auto-merge missed converting to `LOG_ERR` (caused linker error with the new Logging library's `#define Serial` macro)
- **lib/hal/HalPowerManager.cpp** — Converted 4 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, added `#include <Logging.h>`
- **src/util/BookSettings.cpp** — Converted 3 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, replaced `#include <HardwareSerial.h>` with `#include <Logging.h>`
- **src/util/BookmarkStore.cpp** — Converted 2 `Serial.printf` calls to `LOG_ERR`/`LOG_DBG`, added `#include <Logging.h>`
- **platformio.ini** — Added `-DENABLE_SERIAL_LOG` and `-DLOG_LEVEL=2` to `[env:mod]` build flags (was missing, other envs all have these)
## Build Verification
PlatformIO build (`pio run -e mod`) succeeded:
- RAM: 31.0% (101724/327680 bytes)
- Flash: 96.8% (6342796/6553600 bytes)
## Follow-up
- The merge is on branch `mod/merge-upstream` — fast-forward `mod/master` when ready
- The `TxtReaderActivity.cpp` has a pre-existing `[[nodiscard]]` warning for `generateCoverBmp()` (not introduced by this merge)

View File

@@ -0,0 +1,34 @@
# Per-book Letterbox Fill Override
**Date:** 2026-02-13
**Branch:** mod/fix-edge-fills
## Task
Add the ability to override the sleep cover "letterbox fill mode" on a per-book basis (EPUB only for the menu UI; all book types respected at sleep time).
## Changes Made
### New files
- `src/util/BookSettings.h` / `src/util/BookSettings.cpp` — Lightweight per-book settings utility. Stores a `letterboxFillOverride` field (0xFF = use global default) in `{cachePath}/book_settings.bin`. Versioned binary format with field count for forward compatibility, matching the pattern used by BookmarkStore and CrossPointSettings.
### Modified files
- `src/activities/reader/EpubReaderMenuActivity.h` — Added `LETTERBOX_FILL` to the `MenuAction` enum. Added `bookCachePath`, `pendingLetterboxFill`, letterbox fill labels, and helper methods (`letterboxFillToIndex`, `indexToLetterboxFill`, `saveLetterboxFill`). Constructor now accepts a `bookCachePath` parameter and loads the current per-book settings.
- `src/activities/reader/EpubReaderMenuActivity.cpp` — Handle `LETTERBOX_FILL` action: cycles through Default/Dithered/Solid/None on Confirm (handled locally like `ROTATE_SCREEN`), saves immediately. Renders the current value on the right side of the menu item.
- `src/activities/reader/EpubReaderActivity.cpp` — Passes `epub->getCachePath()` to the menu activity constructor. Added `ROTATE_SCREEN` and `LETTERBOX_FILL` to the `onReaderMenuConfirm` switch as no-ops to prevent compiler warnings.
- `src/activities/boot_sleep/SleepActivity.h` — Added `fillModeOverride` parameter to `renderBitmapSleepScreen()`.
- `src/activities/boot_sleep/SleepActivity.cpp``renderCoverSleepScreen()` now loads `BookSettings` from the book's cache path after determining the book type. Passes the per-book override to `renderBitmapSleepScreen()`. `renderBitmapSleepScreen()` uses the override if valid, otherwise falls back to the global `SETTINGS.sleepScreenLetterboxFill`.
## How It Works
1. User opens the EPUB reader menu (Confirm button while reading).
2. "Letterbox Fill" appears between "Reading Orientation" and "Table of Contents".
3. Pressing Confirm cycles: Default → Dithered → Solid → None → Default...
4. The selection is persisted immediately to `book_settings.bin` in the book's cache directory.
5. When the device enters sleep with the cover screen, the per-book override is loaded and used instead of the global setting (if set).
6. XTC and TXT books also have their per-book override checked at sleep time, but can only be configured for EPUB via the reader menu (XTC/TXT lack general menus).
## Follow-up Items
- Consider adding the letterbox fill override to XTC/TXT reader menus if those get general menus in the future.
- The `BookSettings` struct is extensible — other per-book overrides can be added by appending fields and incrementing `BOOK_SETTINGS_COUNT`.

View File

@@ -0,0 +1,36 @@
# Revert Letterbox Fill to Dithered / Solid / None (with edge cache)
**Date:** 2026-02-13
## Task
Reverted letterbox fill modes from None/Solid/Extend Edges back to Dithered (default)/Solid/None per user request. Restored Bayer ordered dithering for "Dithered" mode and re-introduced edge average caching to avoid recomputing on every sleep.
## Changes Made
### `src/CrossPointSettings.h`
- Reordered enum: `LETTERBOX_DITHERED=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_NONE=2`
- Changed default from `LETTERBOX_NONE` to `LETTERBOX_DITHERED`
### `src/SettingsList.h`
- Updated UI labels from `{"None", "Solid", "Extend Edges"}` to `{"Dithered", "Solid", "None"}`
### `src/activities/boot_sleep/SleepActivity.h`
- Added `#include <string>`
- Restored `edgeCachePath` parameter to `renderBitmapSleepScreen()`
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Removed**: `getPackedPixel()`, `setPackedPixel()`, all EXTENDED-mode logic, `freeEdgeData()`, per-pixel edge arrays
- **Simplified** `LetterboxFillData` to just `avgA`, `avgB`, `letterboxA`, `letterboxB`, `horizontal`, `valid`
- **Restored** `BAYER_4X4[4][4]` matrix and `quantizeBayerDither()` function
- **Renamed** `computeEdgeData()``computeEdgeAverages()` (averages-only, no edge pixel capture)
- **Added** edge average cache: `loadEdgeCache()` / `saveEdgeCache()` (~12 byte binary file per cover)
- **Updated** `drawLetterboxFill()`: DITHERED uses Bayer dithering, SOLID uses snap-to-level
- **Updated** `renderBitmapSleepScreen()`: accepts `edgeCachePath`, tries cache before computing
- **Updated** `renderCoverSleepScreen()`: derives `edgeCachePath` from cover BMP path (`_edges.bin`)
## Build
Compilation succeeds (ESP32-C3 target, PlatformIO).
## Follow-up
- The specific "The World in a Grain" cover still has rendering issues with dithered mode — to be investigated separately
- Custom sleep BMPs (`/sleep/` directory, `/sleep.bmp`) intentionally skip caching since the selected BMP can change each sleep

View File

@@ -0,0 +1,50 @@
# Placeholder Cover Generation for Books Without Covers
## Task
Implement placeholder cover BMP generation for books that have no embedded cover image (or have covers in unsupported formats). Previously, these books showed empty rectangles on the home screen and fell back to the default sleep screen.
## Root Cause
Cover generation failed silently in three cases:
- **EPUB**: No `coverItemHref` in metadata, or cover is non-JPG format (PNG/SVG/GIF)
- **TXT**: No matching image file on the SD card
- **XTC**: First-page render failure (rare)
When `generateCoverBmp()` returned `false`, no BMP was created, and no fallback was attempted.
## Changes Made
### New Files
- `lib/PlaceholderCover/PlaceholderCoverGenerator.h` - Header for the placeholder generator
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` - Implementation: allocates a 1-bit pixel buffer, renders title/author text using EpdFont glyph data (ported from GfxRenderer::renderChar), writes a 1-bit BMP file with word-wrapped centered text, border, and separator line
### Modified Files
- `src/activities/reader/EpubReaderActivity.cpp` - Added placeholder fallback after `generateCoverBmp()` and `generateThumbBmp()` fail during first-open prerender
- `src/activities/reader/TxtReaderActivity.cpp` - Added placeholder fallback, added thumbnail generation (previously TXT had none), now passes thumb path to RECENT_BOOKS.addBook() instead of empty string
- `src/activities/reader/XtcReaderActivity.cpp` - Added placeholder fallback after cover/thumb generation fail (rare case)
- `src/activities/boot_sleep/SleepActivity.cpp` - Added placeholder generation before falling back to default sleep screen (handles books opened before this feature was added)
- `lib/Txt/Txt.h` - Added `getThumbBmpPath()` and `getThumbBmpPath(int height)` methods
- `lib/Txt/Txt.cpp` - Implemented the new thumb path methods
### Architecture
- Option B approach: shared `PlaceholderCoverGenerator` utility called from reader activities
- Generator is independent of `GfxRenderer` (no display framebuffer dependency)
- Includes Ubuntu 10 Regular and Ubuntu 12 Bold font data directly for self-contained rendering
- Memory: 48KB for full cover (480x800), 4-12KB for thumbnails; allocated and freed per call
## Flash Impact
- Before: 96.4% flash usage (6,317,250 bytes)
- After: 97.3% flash usage (6,374,546 bytes)
- Delta: ~57KB (mostly from duplicate font bitmap data included in the generator)
## Layout Revision (traditional book cover style)
- Border: moved inward with proportional edge padding (~10px at full size) and thickened to ~5px, keeping it visible within the device bezel
- Title: 2x integer-scaled Ubuntu 12 Bold (effectively ~24pt) for full-size covers, positioned in the top 2/3 zone, max 5 lines
- Author: positioned in the bottom 1/3 zone, max 3 lines with word wrapping
- Book icon: 48x48 1-bit bitmap (generated by `scripts/generate_book_icon.py`), displayed at 2x for full covers, 1x for medium thumbnails, omitted for small thumbnails
- Separator line between title and author zones
- All dimensions scale proportionally for thumbnails
## Follow-up Items
- Books opened before this feature was added won't get home screen thumbnails until re-opened (SleepActivity handles the sleep cover case by generating on demand)
- Font data duplication adds ~57KB flash; could be reduced by exposing shared font references instead of including headers directly
- Preview scripts in `scripts/` can regenerate the icon and layout previews: `generate_book_icon.py`, `preview_placeholder_cover.py`

View File

@@ -0,0 +1,59 @@
# PR #857 Full Feature Update Integration
**Date:** 2026-02-14
## Task Description
Implemented the full feature update from PR #857 ("feat: Add dictionary word lookup feature") into our fork, following the detailed plan in `pr_857_update_integration_190041ae.plan.md`. This covered dictionary intelligence features (stemming, edit distance, fuzzy matching), the `ActivityWithSubactivity` refactor for inline definition display, en-dash/em-dash splitting, cross-page hyphenation, reverse-chronological lookup history, and a new "Did you mean?" suggestions activity.
## Changes Made
### New Files (2)
- **`src/activities/reader/DictionarySuggestionsActivity.h`** — New "Did you mean?" activity header. Adapted from PR with `orientation` parameter for our `DictionaryDefinitionActivity` constructor.
- **`src/activities/reader/DictionarySuggestionsActivity.cpp`** — Suggestions list UI with `UITheme`-aware layout, sub-activity management for definition display.
### Modified Files (9)
1. **`src/util/Dictionary.h`** — Added `getStemVariants()`, `findSimilar()` (public) and `editDistance()` (private) declarations.
2. **`src/util/Dictionary.cpp`** — Added ~250 lines: morphological stemming (`getStemVariants`), Levenshtein distance (`editDistance`), and fuzzy index scan (`findSimilar`). Preserved fork's `/.dictionary/` paths, `stardictCmp`/`asciiCaseCmp`, `cacheExists()`/`deleteCache()`.
3. **`src/activities/reader/DictionaryDefinitionActivity.h`** — Added optional `onDone` callback parameter and member. Enables "Done" button to exit all the way back to the reader.
4. **`src/activities/reader/DictionaryDefinitionActivity.cpp`** — Split Confirm handler: calls `onDone()` if set, else `onBack()`. Button hint shows "Done" when callback provided. Preserved all HTML parsing, styled rendering, side button hints.
5. **`src/activities/reader/DictionaryWordSelectActivity.h`** — Changed base class to `ActivityWithSubactivity`. Replaced `onLookup` callback with `nextPageFirstWord` string. Added `pendingBackFromDef`/`pendingExitToReader` state.
6. **`src/activities/reader/DictionaryWordSelectActivity.cpp`** — Major update:
- En-dash/em-dash splitting in `extractWords()` (splits on U+2013/U+2014)
- Cross-page hyphenation in `mergeHyphenatedWords()` using `nextPageFirstWord`
- Cascading lookup flow: exact → stem variants → similar suggestions → "Not found"
- Sub-activity delegation in `loop()` for definition/suggestions screens
- Preserved custom `drawHints()` with overlap detection and `PageForward`/`PageBack` support
7. **`src/activities/reader/LookedUpWordsActivity.h`** — Replaced `onSelectWord` with `onDone` callback. Added `readerFontId`, `orientation`, `pendingBackFromDef`/`pendingExitToReader`, `getPageItems()`.
8. **`src/activities/reader/LookedUpWordsActivity.cpp`** — Major rewrite:
- Reverse-chronological word display
- Inline cascading lookup flow (same as word select)
- `UITheme`-aware layout with `GUI.drawHeader()`/`GUI.drawList()`
- `onNextRelease`/`onPreviousRelease`/`onNextContinuous`/`onPreviousContinuous` navigation
- Sub-activity management for definition/suggestions
- Preserved delete confirmation mode
9. **`src/activities/reader/EpubReaderActivity.cpp`** — Simplified LOOKUP handler (removed `onLookup` callback, added `nextPageFirstWord` extraction). Simplified LOOKED_UP_WORDS handler (removed inline lookup, passes `readerFontId` and `orientation`). Removed unused `LookupHistory.h` include.
### Cleanup
- Removed unused `DictionaryDefinitionActivity.h` include from `EpubReaderActivity.h`
- Removed unused `util/LookupHistory.h` include from `EpubReaderActivity.cpp`
## Architectural Summary
**Before:** `EpubReaderActivity` orchestrated definition display via callbacks — word select and history both called back to the reader to create definition activities.
**After:** `DictionaryWordSelectActivity` and `LookedUpWordsActivity` manage their own sub-activity chains (definition, suggestions) using `ActivityWithSubactivity`. This enables the cascading lookup flow: exact match → stem variants → similar suggestions → "Not found".
## What Was Preserved (Fork Advantages)
- Full HTML parsing in `DictionaryDefinitionActivity`
- Custom `drawHints()` with overlap detection in `DictionaryWordSelectActivity`
- `PageForward`/`PageBack` button support in word selection
- `DELETE_DICT_CACHE` menu item and `cacheExists()`/`deleteCache()`
- `stardictCmp`/`asciiCaseCmp` for proper StarDict index comparison
- `/.dictionary/` path prefix
## Follow-up Items
- Test the full lookup flow on device (exact → stems → suggestions → not found)
- Verify cross-page hyphenation with a book that has page-spanning hyphenated words
- Verify en-dash/em-dash splitting with books using those characters
- Confirm reverse-chronological history order is intuitive for users

View File

@@ -0,0 +1,29 @@
# Placeholder Cover Visual Refinements & Home Screen Integration
## Task
Refined the placeholder cover layout to match a mockup, and integrated placeholder generation into the home screen's thumbnail loading.
## Changes Made
### Layout Refinements (`PlaceholderCoverGenerator.cpp`, `preview_placeholder_cover.py`)
- **Icon position**: Moved from above-title to side-by-side (icon left, title right)
- **Author scale**: Increased from 1x to 2x on full-size covers for larger author text
- **Line spacing**: Reduced to 75% of advanceY so 2-3 title lines fit within icon height
- **Vertical centering**: Title text centers against icon when 1-2 lines; top-aligns with overflow for 3+ lines. Uses `ascender`-based visual height instead of `advanceY`-based for accurate centering
- **Horizontal centering**: The icon+gap+text block is now centered as a unit based on actual rendered text width, not the full available text area
### Home Screen Integration (`HomeActivity.cpp`)
- Added `PlaceholderCoverGenerator` fallback in `loadRecentCovers()` — when format-specific `generateThumbBmp()` fails (or for TXT which had no handler), a placeholder thumbnail is generated instead of clearing `coverBmpPath` and showing a blank rectangle
- This covers the case where a book was previously opened, cache was cleared, and the home screen needs to regenerate thumbnails
## Files Changed
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — layout logic updates
- `scripts/preview_placeholder_cover.py` — matching preview updates
- `src/activities/home/HomeActivity.cpp` — placeholder fallback in loadRecentCovers
## Commit
`632b76c` on `mod/generate-placeholder-covers`
## Follow-up Items
- Test on actual device to verify C++ bitmap font rendering matches preview expectations
- The preview script uses Helvetica (different metrics than ubuntu_12_bold), so on-device appearance will differ slightly from previews

View File

@@ -0,0 +1,55 @@
# Flash Size Optimization: Per-Family Font and Per-Language Hyphenation Flags
**Date**: 2026-02-15
## Task Description
Investigated ESP32-C3 flash usage (97.3% full at 6.375 MB / 6.5 MB app partition) and implemented build flags to selectively exclude font families and hyphenation language tries. Fonts alone consumed 65.6% of flash (3.99 MB).
## Changes Made
### 1. `lib/EpdFont/builtinFonts/all.h`
- Wrapped Bookerly includes (16 files) in `#ifndef OMIT_BOOKERLY`
- Wrapped Noto Sans 12-18pt includes (16 files) in `#ifndef OMIT_NOTOSANS` (kept `notosans_8_regular.h` outside guard for UI small font)
- Wrapped OpenDyslexic includes (16 files) in `#ifndef OMIT_OPENDYSLEXIC`
### 2. `src/main.cpp`
- Wrapped Bookerly 14pt font objects (previously always included) in `#ifndef OMIT_BOOKERLY`
- Added per-family `#ifndef OMIT_*` guards inside the existing `#ifndef OMIT_FONTS` block for other sizes
- Added matching guards to font registration in `setupDisplayAndFonts()`
### 3. `src/SettingsList.h`
- Added `FontFamilyMapping` struct and `kFontFamilyMappings[]` compile-time table with per-family `#ifndef` guards
- Switched Font Family setting from `SettingInfo::Enum` to `SettingInfo::DynamicEnum` with getter/setter that maps between list indices and fixed enum values (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2)
- Added `static_assert` ensuring at least one font family is available
### 4. `src/activities/settings/SettingsActivity.cpp`
- Added DynamicEnum toggle support in `toggleCurrentSetting()` (cycles through options via `valueGetter`/`valueSetter`)
- Added DynamicEnum display support in `render()` display lambda
### 5. `src/CrossPointSettings.cpp`
- Guarded `getReaderFontId()` switch cases with `#ifndef OMIT_*`, added `default:` fallback to first available font
- Guarded `getReaderLineCompression()` switch cases with `#ifndef OMIT_*`, added `default:` fallback
- Added `#error` directive if all font families are omitted
### 6. `lib/Epub/Epub/hyphenation/LanguageRegistry.cpp`
- Added per-language `#ifndef OMIT_HYPH_xx` guards around includes, `LanguageHyphenator` objects, and entries
- Switched from `std::array<LanguageEntry, 6>` to `std::vector<LanguageEntry>` for variable entry count
- Languages: DE (201 KB), EN (27 KB), ES (13 KB), FR (7 KB), IT (2 KB), RU (33 KB)
### 7. `platformio.ini`
- Added to `[env:mod]`: `-DOMIT_OPENDYSLEXIC`, `-DOMIT_HYPH_DE`, `-DOMIT_HYPH_EN`, `-DOMIT_HYPH_ES`, `-DOMIT_HYPH_FR`, `-DOMIT_HYPH_IT`, `-DOMIT_HYPH_RU`
## Design Decisions
- Enum values stay fixed (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2) for settings file compatibility
- Existing `OMIT_FONTS` flag left untouched; per-family flags nest inside it
- DynamicEnum used for Font Family to handle index-to-value mapping when fonts are removed from the middle of the options list
## Estimated Savings (mod build)
- OpenDyslexic fonts: ~1,052 KB
- All hyphenation tries: ~282 KB
- **Total: ~1,334 KB (~1.30 MB)** -- from 97.3% down to ~76.9%
## Follow-up Items
- Build and verify the `mod` environment compiles cleanly and flash size reduction matches estimates
- Other available flags for future use: `OMIT_BOOKERLY`, `OMIT_NOTOSANS` (individual language OMIT_HYPH_xx flags can also be used selectively)

View File

@@ -0,0 +1,30 @@
# Table Rendering Fixes: &nbsp; Entities and Colspan Support
## Task Description
Fix two issues with the newly implemented EPUB table rendering:
1. Stray `&nbsp;` entities appearing as literal text in table cells instead of whitespace
2. Cells with `colspan` attributes (e.g., section headers like "Anders Celsius", "Scientific career") rendering as narrow single-column cells instead of spanning the full table width
## Changes Made
### 1. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `flushPartWordBuffer()`
- Added detection and replacement of literal `&nbsp;` strings in the word buffer before flushing to `ParsedText`
- This handles double-encoded `&amp;nbsp;` entities common in Wikipedia and other generated EPUBs, where XML parsing converts `&amp;` to `&` leaving literal `&nbsp;` in the character data
### 2. `lib/Epub/Epub/TableData.h` — `TableCell` struct
- Added `int colspan = 1` field to store the HTML `colspan` attribute value
### 3. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `startElement()`
- Added parsing of the `colspan` attribute from `<td>` and `<th>` tags
- Stores the parsed value (minimum 1) in the `TableCell::colspan` field
### 4. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `processTable()`
- **Column count**: Changed from `max(row.cells.size())` to sum of `cell.colspan` per row, correctly determining logical column count
- **Natural width measurement**: Only non-spanning cells (colspan=1) contribute to per-column width calculations; spanning cells use combined width
- **Layout**: Added `spanContentWidth()` and `spanFullCellWidth()` lambdas to compute the combined content width and full cell width for cells spanning multiple columns
- **Cell mapping**: Each `PageTableCellData` now maps to an actual cell (not a logical column), with correct x-offset and combined column width for spanning cells
- **Fill logic**: Empty cells are appended only for unused logical columns after all actual cells are placed
## Follow-up Items
- Rowspan support is not yet implemented (uncommon in typical EPUB content)
- The `&nbsp;` fix only handles the most common double-encoded entity; other double-encoded entities (e.g., `&amp;mdash;`) could be handled similarly if needed

View File

@@ -0,0 +1,49 @@
# Table Rendering Tweaks: Centering, Line Breaks, Padding
## Task Description
Three visual tweaks to the EPUB table rendering based on comparison with a Wikipedia article (Anders Celsius):
1. Full-width spanning rows (like "Anders Celsius", "Scientific career", "Signature") should be center-aligned
2. `<br>` tags and block elements within table cells should create actual line breaks (e.g., "(aged 42)" then "Uppsala, Sweden" on the next line)
3. Cell padding was too tight — text too close to grid borders
## Changes Made
### 1. Forced Line Breaks in Table Cells
**`lib/Epub/Epub/ParsedText.h`**:
- Added `std::vector<bool> forceBreakAfter` member to track mandatory line break positions
- Added `void addLineBreak()` public method
**`lib/Epub/Epub/ParsedText.cpp`**:
- `addWord()`: grows `forceBreakAfter` vector alongside other word vectors
- `addLineBreak()`: sets `forceBreakAfter.back() = true` on the last word
- `computeLineBreaks()` (DP algorithm): respects forced breaks — cannot extend a line past a forced break point, and forced breaks override continuation groups
- `computeHyphenatedLineBreaks()` (greedy): same — stops line at forced break, won't backtrack past one
- `hyphenateWordAtIndex()`: when splitting a word, transfers the forced break flag to the remainder (last part)
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `startNewTextBlock()`:
- When `inTable`, instead of being a no-op, now: flushes the word buffer, calls `addLineBreak()` on the current ParsedText, and resets `nextWordContinues`
- This means `<br>`, `<p>`, `<div>` etc. within table cells now produce real visual line breaks
### 2. Center-Aligned Full-Width Spanning Cells
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `processTable()`:
- Before laying out a cell's content, checks if `cell.colspan >= numCols` (spans full table width)
- If so, sets the cell's BlockStyle alignment to `CssTextAlign::Center`
- This correctly centers section headers and title rows in Wikipedia infobox-style tables
### 3. Increased Cell Padding
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**:
- `TABLE_CELL_PAD_X`: 2 → 4 pixels (horizontal padding)
- Added `TABLE_CELL_PAD_Y = 2` pixels (vertical padding)
- Row height now includes `2 * TABLE_CELL_PAD_Y` for top/bottom padding
**`lib/Epub/Epub/Page.cpp`**:
- `TABLE_CELL_PADDING_X`: 2 → 4 pixels (matches parser constant)
- Added `TABLE_CELL_PADDING_Y = 2` pixels
- Cell text Y position now accounts for vertical padding: `baseY + 1 + TABLE_CELL_PADDING_Y`
## Follow-up Items
- The padding constants are duplicated between `ChapterHtmlSlimParser.cpp` and `Page.cpp` — could be unified into a shared header
- Vertical centering within cells (when a cell has fewer lines than the tallest cell) is not implemented

View File

@@ -0,0 +1,32 @@
# Table Width Hints: HTML Attributes + CSS Width Support
## Task Description
Add support for author-specified column widths from HTML `width` attributes and CSS `width` property on `<table>`, `<col>`, `<td>`, and `<th>` elements, using them as hints for column sizing in `processTable()`.
## Changes Made
### 1. CSS Layer (`lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`)
- Added `width` bit to `CssPropertyFlags`
- Added `CssLength width` field to `CssStyle`
- Added `hasWidth()` convenience method
- Updated `applyOver()`, `reset()`, `clearAll()`, `anySet()` to include `width`
- Added `else if (propName == "width")` case to `CssParser::parseDeclarations()` using `interpretLength()`
### 2. Table Data (`lib/Epub/Epub/TableData.h`)
- Added `CssLength widthHint` and `bool hasWidthHint` to `TableCell`
- Added `std::vector<CssLength> colWidthHints` to `TableData` (from `<col>` tags)
- Added `#include "css/CssStyle.h"` for `CssLength`
### 3. Parser (`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`)
- Removed `"col"` from `TABLE_SKIP_TAGS` (was being skipped entirely)
- Added `parseHtmlWidthAttr()` helper: parses HTML `width="200"` (pixels) and `width="50%"` (percent) into `CssLength`
- Added `<col>` handling in `startElement`: parses `width` HTML attribute and `style` CSS, pushes to `tableData->colWidthHints`
- Updated `<td>`/`<th>` handling: now parses `width` HTML attribute, `style` attribute, and stylesheet CSS width (via `resolveStyle`). CSS takes priority over HTML attribute. Stored in `TableCell::widthHint`
### 4. Layout (`processTable()`)
- Added step 3a: resolves width hints per column. Priority: `<col>` hints > max cell hint (colspan=1 only). Percentages resolve relative to available content width.
- Modified step 3b: hinted columns get their resolved pixel width (clamped to min col width). If all hinted widths exceed available space, they're scaled down proportionally. Unhinted columns use the existing two-pass fair-share algorithm on the remaining space.
## Follow-up Items
- The `<colgroup>` tag is still skipped entirely; `<col>` tags within `<colgroup>` won't be reached. Could un-skip `<colgroup>` (make it transparent like `<thead>`/`<tbody>`) if needed.
- `rowspan` is not yet supported.

View File

@@ -0,0 +1,41 @@
# Cherry-pick Image Support from pablohc/crosspoint-reader@2d8cbcf (PR #556)
## Task
Merge EPUB embedded image support (JPEG/PNG) from pablohc's fork into the mod branch, based on upstream PR #556.
## Changes Made
### New Files (11)
- `lib/Epub/Epub/blocks/ImageBlock.h` / `.cpp` - Image block type for page layout
- `lib/Epub/Epub/converters/DitherUtils.h` - 4x4 Bayer dithering for 4-level grayscale
- `lib/Epub/Epub/converters/ImageDecoderFactory.h` / `.cpp` - Format-based decoder selection
- `lib/Epub/Epub/converters/ImageToFramebufferDecoder.h` / `.cpp` - Base decoder interface
- `lib/Epub/Epub/converters/JpegToFramebufferConverter.h` / `.cpp` - JPEG decoder (picojpeg)
- `lib/Epub/Epub/converters/PngToFramebufferConverter.h` / `.cpp` - PNG decoder (PNGdec)
- `lib/Epub/Epub/converters/PixelCache.h` - 2-bit pixel cache for fast re-render
- `scripts/generate_test_epub.py` - Test EPUB generator
### Modified Files (13)
- `lib/Epub/Epub/blocks/Block.h` - Removed unused `layout()` virtual
- `lib/Epub/Epub/blocks/TextBlock.h` - Removed unused `layout()` override
- `lib/Epub/Epub/Page.h` / `.cpp` - Added `PageImage` class, `TAG_PageImage=3`, `hasImages()`, `getImageBoundingBox()`
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` / `.cpp` - Image extraction/decoding from EPUB, new constructor params
- `lib/Epub/Epub/Section.cpp` - Derive content base/image paths, bumped version 12→13
- `lib/GfxRenderer/GfxRenderer.h` / `.cpp` - Added `getRenderMode()`, implemented `displayWindow()`
- `lib/hal/HalDisplay.h` / `.cpp` - Added `displayWindow()` for partial refresh
- `src/activities/reader/EpubReaderActivity.cpp` - Image-aware refresh with double FAST_REFRESH optimization
- `platformio.ini` - Added `PNGdec` dependency, `PNG_MAX_BUFFERED_PIXELS=6402` build flag
## Key Conflict Resolutions
- `TAG_PageImage = 3` (not 2) to avoid collision with mod's `TAG_PageTableRow = 2`
- Preserved mod's bookmark ribbon rendering in `renderContents`
- Preserved mod's table rendering (`PageTableRow`) alongside new `PageImage`
- Section file version bumped to invalidate cached sections
## Build Result
- `mod` environment: SUCCESS (RAM 31.0%, Flash 77.5%)
## Follow-up Items
- Test on device with JPEG/PNG EPUBs
- Run `scripts/generate_test_epub.py` to create test EPUBs
- Consider whether `displayWindow()` experimental path should be enabled

View File

@@ -0,0 +1,52 @@
# EPUB Table Rendering Implementation
## Task
Replace the `[Table omitted]` placeholder in the EPUB reader with full column-aligned table rendering, including grid lines, proportional column widths, and proper serialization.
## Changes Made
### New file
- **`lib/Epub/Epub/TableData.h`** -- Lightweight structs (`TableCell`, `TableRow`, `TableData`) for buffering table content during SAX parsing.
### Modified files
- **`lib/Epub/Epub/ParsedText.h` / `.cpp`**
- Added `getNaturalWidth()` public method to measure the single-line content width of a ParsedText. Used by column width calculation.
- **`lib/Epub/Epub/Page.h` / `.cpp`**
- Added `TAG_PageTableRow = 2` to `PageElementTag` enum.
- Added `getTag()` pure virtual method to `PageElement` base class for tag-based serialization.
- Added `PageTableCellData` struct (cell lines, column width, x-offset).
- Added `PageTableRow` class with render (grid lines + cell text), serialize, and deserialize support.
- Updated `Page::serialize()` to use `el->getTag()` instead of hardcoded tag.
- Updated `Page::deserialize()` to handle `TAG_PageTableRow`.
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h`**
- Added `#include "../TableData.h"`.
- Added table state fields: `bool inTable`, `std::unique_ptr<TableData> tableData`.
- Added `processTable()` and `addTableRowToPage()` method declarations.
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**
- Added table-related tag arrays (`TABLE_TRANSPARENT_TAGS`, `TABLE_SKIP_TAGS`).
- Replaced `[Table omitted]` placeholder with full table buffering logic in `startElement`.
- Modified `startNewTextBlock` to be a no-op when inside a table (cell content stays in one ParsedText).
- Added table close handling in `endElement` for `</td>`, `</th>`, and `</table>`.
- Disabled the 750-word early split when inside a table.
- Implemented `processTable()`: column width calculation (natural + proportional distribution), per-cell layout via `layoutAndExtractLines`, `PageTableRow` creation.
- Implemented `addTableRowToPage()`: page-break handling for table rows.
## Design Decisions
- Tables are buffered entirely during parsing, then processed on `</table>` close (two-pass: measure then layout).
- Column widths are proportional to natural content width, with equal distribution of extra space when content fits.
- Grid lines (1px) drawn around every cell; 2px horizontal cell padding.
- Nested tables are skipped (v1 limitation).
- `<caption>`, `<colgroup>`, `<col>` are skipped; `<thead>`, `<tbody>`, `<tfoot>` are transparent.
- `<th>` cells get bold text. Cell text is left-aligned with no paragraph indent.
- Serialization is backward-compatible: old firmware encountering the new tag will re-parse the section.
## Follow-up Items
- Nested table support (currently skipped)
- `colspan` / `rowspan` support
- `<caption>` rendering as centered text above the table
- CSS border detection (currently always draws grid lines)
- Consider CSS-based cell alignment

View File

@@ -0,0 +1,39 @@
# Adjust Low Power Mode: Fix Processing Bug and Sync with PR #852
**Date:** 2026-02-15
**Branch:** mod/adjust-low-power-mode
## Task
Fix a bug where the device enters low-power mode (10MHz CPU) during first-time book opening and chapter indexing, causing significant slowdown. Also sync minor logging differences with upstream PR #852.
## Root Causes (three issues combined)
1. **Missing delegation in ActivityWithSubactivity**: `main.cpp` calls `preventAutoSleep()` on `ReaderActivity` (the top-level activity). `ReaderActivity` creates `EpubReaderActivity` as a subactivity, but `ActivityWithSubactivity` never delegated `preventAutoSleep()` or `skipLoopDelay()` to the active subactivity.
2. **Stale check across activity transitions**: The `preventAutoSleep()` check at the top of the main loop runs before `loop()`. When an activity transitions mid-loop (HomeActivity -> ReaderActivity), the pre-loop check is stale but the post-loop power-saving decision fires.
3. **Section object vs section file**: `!section` alone was insufficient as a condition. The Section object is created early in the `!section` block, making `section` non-null, but `createSectionFile()` (the slow operation) runs afterward. A separate `loadingSection` flag is needed to cover the full duration.
## Changes Made
1. **ActivityWithSubactivity** (`src/activities/ActivityWithSubactivity.h`)
- Added `preventAutoSleep()` override that delegates to `subActivity->preventAutoSleep()`
- Added `skipLoopDelay()` override with same delegation pattern
2. **main.cpp** (`src/main.cpp`)
- Added a second `preventAutoSleep()` re-check after `currentActivity->loop()` returns, before the power-saving block
3. **EpubReaderActivity** (`src/activities/reader/EpubReaderActivity.h`, `.cpp`)
- Added `volatile bool loadingSection` flag
- `preventAutoSleep()` returns `!section || loadingSection`
- `!section` covers the pre-Section-object period (including cover prerendering in onEnter)
- `loadingSection` covers the full `!section` block in `renderScreen()` where `createSectionFile()` runs
- Flag is also cleared on the error path
4. **TxtReaderActivity** (`src/activities/reader/TxtReaderActivity.h`)
- `preventAutoSleep()` returns `!initialized`
- Covers cover prerendering and page index building
5. **HalPowerManager.cpp** (`lib/hal/HalPowerManager.cpp`)
- Synced log messages with upstream PR: `LOG_ERR` -> `LOG_DBG` with frequency values (matching commit `ff89fb1`)

View File

@@ -0,0 +1,28 @@
# Fix: Cover/Thumbnail Pipeline on Home Screen
**Date:** 2026-02-15
## Task Description
Multiple issues with book cover thumbnails on the home screen:
1. After clearing a book's cache, the home screen showed a placeholder instead of the real cover.
2. Books without covers showed blank rectangles instead of generated placeholder covers.
## Root Cause
`Epub::generateThumbBmp()` wrote an empty 0-byte BMP file as a "don't retry" sentinel when a book had no cover. This empty file:
- Blocked the placeholder fallback in `EpubReaderActivity::onEnter()` (file exists check passes)
- Tricked the home screen into thinking a valid thumbnail exists (skips regeneration)
- Failed to parse in `LyraTheme::drawRecentBookCover()` resulting in a blank gray rectangle
## Changes Made
- **`lib/Epub/Epub.cpp`**: Removed the empty sentinel file write from `generateThumbBmp()`. Now it simply returns `false` when there's no cover, letting callers generate valid placeholder BMPs that serve the same "don't retry" purpose.
- **`src/activities/home/HomeActivity.cpp`**: Changed placeholder fallback in `loadRecentCovers()` from `if (!success && !Storage.exists(coverPath))` to `if (!success)` as defense-in-depth for edge cases like global cache clear.
- **`src/RecentBooksStore.h`**: Added `removeBook(const std::string& path)` method declaration.
- **`src/RecentBooksStore.cpp`**: Implemented `removeBook()` — finds and erases the book by path, then persists the updated list.
- **`src/activities/reader/EpubReaderActivity.cpp`**: After clearing cache in the `DELETE_CACHE` handler, calls `RECENT_BOOKS.removeBook(epub->getPath())` so the book is cleanly removed from recents when its cache is wiped.
## Follow-up Items
- None.

View File

@@ -0,0 +1,42 @@
# Merge upstream master (CSS perf #779) into mod/master-img
**Date**: 2026-02-15
## Task
Merge the latest changes from `master` (upstream) into `mod/master-img`.
## Changes Merged
One upstream commit: `46c2109 perf: Improve large CSS files handling (#779)`
This commit significantly refactored the CSS subsystem:
- Streaming CSS parser with `StackBuffer` for zero-heap parsing
- Extracted `parseDeclarationIntoStyle()` from inline logic
- Rule limits and selector validation
- `CssParser` now owns its `cachePath` and manages caching internally
- CSS loading skipped when "Book's Embedded Style" is off
## Conflicts Resolved
### Section.cpp (2 regions)
- Combined mod's image support variables (`contentBase`, `imageBasePath`) with master's new CSS parser loading pattern (`cssParser->loadFromCache()`)
- Merged constructor call: kept mod's `epub`, `contentBase`, `imageBasePath` params while adopting master's `cssParser` local variable pattern
- Added master's `cssParser->clear()` calls on error/success paths
### CssParser.cpp (1 region)
- Accepted master's complete rewrite of the CSS parser
- Ported mod's `width` CSS property handler into the new `parseDeclarationIntoStyle()` function
## Auto-merged Files Verified
- `CssStyle.h`: `width` property and supporting code preserved
- `platformio.ini`: `PNGdec` library and `PNG_MAX_BUFFERED_PIXELS` preserved; `[env:mod]` section intact; master's `gnu++2a` and `build_unflags` applied
- `ChapterHtmlSlimParser.h`: image/table support members preserved
- `RecentBooksStore.cpp`: `removeBook()` method preserved
- `Epub.cpp`, `Epub.h`, `CssParser.h`, `ReaderActivity.cpp`: auto-merged cleanly
## Build Result
- `pio run -e mod` succeeded with zero errors
## Commit
- `744d616 Merge branch 'master' into mod/master-img`

View File

@@ -0,0 +1,51 @@
# Port PR #838 and PR #907 into fork
## Task
Cherry-pick / manually port two upstream PRs into the fork:
- **PR #907**: Cover image outlines to improve legibility
- **PR #838**: Fallback logic for epub cover extraction
## Changes Made
### PR #907 (cover outlines) — `src/components/themes/lyra/LyraTheme.cpp`
- Always draw a rectangle outline around cover tiles before drawing the bitmap on top
- Removed `hasCover` flag — simplified logic so outline is always present, preventing low-contrast covers from blending into the background
### PR #838 (cover fallback logic) — 3 files
#### `lib/Epub/Epub.h`
- Added declarations for 4 new methods: `generateInvalidFormatCoverBmp`, `generateInvalidFormatThumbBmp`, `isValidThumbnailBmp` (static), `getCoverCandidates` (private)
- Added doc comments on existing `generateCoverBmp` and `generateThumbBmp`
#### `lib/Epub/Epub.cpp`
- Added `#include <HalDisplay.h>` and `#include <algorithm>`
- **`generateCoverBmp`**: Added invalid BMP detection/retry, cover fallback candidate probing (`getCoverCandidates`), case-insensitive extension checking. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
- **`generateThumbBmp`**: Same changes as `generateCoverBmp` — invalid BMP detection, fallback probing, case-insensitive check. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
- **`generateInvalidFormatThumbBmp`** (new): Creates 1-bit BMP with X pattern as thumbnail marker
- **`generateInvalidFormatCoverBmp`** (new): Creates 1-bit BMP with X pattern as cover marker, using display dimensions
- **`isValidThumbnailBmp`** (new, static): Validates BMP by checking file size > 0 and 'BM' header
- **`getCoverCandidates`** (new, private): Returns list of common cover filenames to probe
#### `src/activities/home/HomeActivity.cpp`
- Replaced `Storage.exists()` check with `Epub::isValidThumbnailBmp()` to catch corrupt/empty thumbnails
- Added epub.load retry logic (cache-only first, then full build)
- After successful thumbnail generation, update `RECENT_BOOKS` with the new thumb path
- Thumbnail fallback chain: Real Cover → PlaceholderCoverGenerator → X-Pattern marker (epub only; xtc/other formats fall back to placeholder only)
## Adaptations from upstream PR
- All `Serial.printf` calls converted to `LOG_DBG`/`LOG_ERR` macros (fork convention)
- No `#include <HardwareSerial.h>` (not needed with Logging.h)
- Retained fork's `bookMetadataCache` null-check guards
- HomeActivity changes manually adapted to fork's `loadRecentCovers()` structure (upstream has inline code in a different method)
- Fork's `PlaceholderCoverGenerator` is the preferred fallback; X-pattern marker is last resort only
#### `src/activities/boot_sleep/SleepActivity.cpp`
- EPUB sleep screen cover now follows Real Cover → Placeholder → X-Pattern fallback chain
- Upgraded `Storage.exists()` check to `Epub::isValidThumbnailBmp()` for the epub cover path
#### `src/activities/reader/EpubReaderActivity.cpp`
- Cover and thumbnail pre-rendering now follows Real Cover → Placeholder → X-Pattern fallback chain
- Upgraded all `Storage.exists()` checks to `Epub::isValidThumbnailBmp()` for cover/thumb paths
## Follow-up Items
- Build and test on device to verify cover generation pipeline works end-to-end

View File

@@ -0,0 +1,29 @@
# Created CrossPoint Reader Development Skill
**Date**: 2026-02-16
**Task**: Create a Cursor agent skill for CrossPoint Reader firmware development guidance.
## Changes Made
Created project-level skill at `.cursor/skills/crosspoint-reader-dev/` with 6 files:
| File | Lines | Purpose |
|------|-------|---------|
| `SKILL.md` | 202 | Core rules: agent identity, hardware constraints, resource protocol, architecture overview, HAL usage, coding standards, error handling, activity lifecycle, UI rules |
| `architecture.md` | 138 | Build system (PlatformIO CLI + VS Code), build flags, environments, generated files, local config, platform detection |
| `coding-patterns.md` | 135 | FreeRTOS tasks, malloc patterns, global font loading, button mapping, UI rendering rules |
| `debugging-and-testing.md` | 148 | Build commands, serial monitoring, crash debugging (OOM, stack overflow, use-after-free, watchdog), testing checklist, CI/CD pipeline |
| `git-workflow.md` | 98 | Repository detection, branch naming, commit messages, when to commit |
| `cache-management.md` | 100 | SD card cache structure, invalidation rules, file format versioning |
## Design Decisions
- **Progressive disclosure**: SKILL.md kept to 202 lines (well under 500 limit) with always-needed info; detailed references in separate files one level deep
- **Project-level storage**: `.cursor/skills/` so it's shared with anyone using the repo
- **Description** includes broad trigger terms: ESP32-C3, PlatformIO, EPUB, e-ink, HAL, activity lifecycle, FreeRTOS, SD card, embedded C++
## Follow-up Items
- Consider adding the skill directory to `.gitignore` if this should remain personal, or commit if sharing with collaborators
- Update cache file format version numbers if they've changed since the guide was written
- Skill will auto-activate when the agent detects firmware/embedded development context

View File

@@ -0,0 +1,57 @@
# Reader Menu Improvements
## Task
Overhaul the EPUB reader menu: consolidate dictionary actions behind long-press, add portrait/landscape toggle with preferred-orientation settings, add font size cycling, and rename several menu labels.
## Changes Made
### 1. Consolidated Dictionary Menu Items
- Removed "Lookup Word History" and "Delete Dictionary Cache" from the reader menu
- Long-pressing Confirm on "Lookup Word" now opens the Lookup History screen
- "Delete Dictionary Cache" moved to a sentinel entry at the bottom of the Lookup History word list
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `LookedUpWordsActivity.h`, `LookedUpWordsActivity.cpp`, `EpubReaderActivity.cpp`
### 2. Toggle Portrait/Landscape
- Renamed "Reading Orientation" to "Toggle Portrait/Landscape" in the reader menu (kept the original name in global Settings)
- Short-press now toggles between preferred portrait and preferred landscape orientations
- Long-press opens a popup sub-menu with all 4 orientation options
- Added `preferredPortrait` and `preferredLandscape` settings to `CrossPointSettings` (serialized at end for backward-compat)
- Added corresponding settings to `SettingsList.h` using `DynamicEnum` (maps non-sequential enum values correctly)
**Files:** `CrossPointSettings.h`, `CrossPointSettings.cpp`, `SettingsList.h`, `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`
### 3. Toggle Font Size
- Added new `TOGGLE_FONT_SIZE` menu action
- Cycles through Small → Medium → Large → Extra Large → Small on each press
- Shows current size value next to the label (like orientation)
- Applied on menu exit via extended `onBack` callback `(uint8_t orientation, uint8_t fontSize)`
- Added `applyFontSize()` to `EpubReaderActivity` (saves to settings, resets section for re-layout)
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
### 4. Label Renames
- "Letterbox Fill" → "Override Letterbox Fill" (reader menu only; global setting keeps original name)
- "Sync Progress" → "Sync Reading Progress"
**Files:** `english.yaml` (I18n source), regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
### 5. Long-Press Safety
- Added `ignoreNextConfirmRelease` flag to `EpubReaderMenuActivity` to prevent stale releases
- Added `initialSkipRelease` constructor parameter to `LookedUpWordsActivity`
- Extended `ignoreNextConfirmRelease` guard to cover the word-selection path (not just delete-confirm mode)
- Orientation sub-menu also uses `ignoreNextConfirmRelease` to avoid selecting on long-press release
### 6. New I18n Strings
- `STR_TOGGLE_ORIENTATION`: "Toggle Portrait/Landscape"
- `STR_TOGGLE_FONT_SIZE`: "Toggle Font Size"
- `STR_OVERRIDE_LETTERBOX_FILL`: "Override Letterbox Fill"
- `STR_PREFERRED_PORTRAIT`: "Preferred Portrait"
- `STR_PREFERRED_LANDSCAPE`: "Preferred Landscape"
## Build Status
Successfully compiled (default environment). RAM: 31.1%, Flash: 99.4%.
## Follow-up Items
- Translations: New strings fall back to English for all non-English languages. Translators can add entries to their respective YAML files.
- The `SettingInfo::Enum` approach doesn't work for non-sequential enum values (portrait=0, inverted=2). Used `DynamicEnum` with getter/setter lambdas instead.

View File

@@ -0,0 +1,31 @@
# Merge Assessment & Cherry-pick: master -> mod/master
## Task
Assess and merge new commits from `master` into `mod/master`.
## Analysis
- 19 commits on `master` not on `mod/master`, but 16 were already cherry-picked or manually ported (different hashes, same content)
- A full `git merge master` produced 30+ conflicts due to duplicate cherry-picks with different patch IDs
- Identified 3 genuinely new commits
## Changes Made
### 1. Cherry-pick `97c3314` (#932) - `f21720d`
- perf: Skip constructing unnecessary `std::string` in TextBlock.cpp
- 1-line change, applied cleanly
### 2. Cherry-pick `2a32d8a` (#926) - `424e332`
- chore: Improve Russian language support
- Renamed `russia.yaml` -> `russian.yaml`, updated `i18n.md`, fixed translation strings
- Applied cleanly
### 3. Cherry-pick `0bc6747` (#827) - `61fb11c`
- feat: Add PNG cover image support for EPUB books
- Added `PngToBmpConverter` library (new files, 858 lines)
- Resolved 2 conflicts:
- `Epub.cpp`: Discarded incoming JPG/PNG block (used old variable names), added PNG thumbnail support to mod's existing structure using `effectiveCoverImageHref` with case-insensitive checks. Fixed `generateCoverBmp()` PNG block to also use `effectiveCoverImageHref`. Added `.png` to `getCoverCandidates()`.
- `ImageToFramebufferDecoder.cpp`: Took upstream `LOG_ERR` version over mod's `Serial.printf`
## Follow-up Items
- Build and test PNG cover rendering on device
- `mod/master` is now fully caught up with `master`

View File

@@ -0,0 +1,66 @@
# Improve Home Screen
## Task Description
Enhance the Lyra theme home screen with six improvements: empty-state placeholder, adaptive recent book cards, 2-line title wrapping with author, adjusted button positioning, optional clock display, and a manual Set Time activity.
## Changes Made
### i18n Strings (8 YAML files + auto-generated C++ files)
- Added `STR_CHOOSE_SOMETHING`, `STR_HOME_SCREEN_CLOCK`, `STR_CLOCK_AMPM`, `STR_CLOCK_24H`, `STR_SET_TIME` to all 8 translation YAML files with localized text
- Regenerated `lib/I18n/I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp` via `gen_i18n.py`
### Settings: Clock Format (`CrossPointSettings.h`, `.cpp`, `SettingsList.h`)
- Added `CLOCK_FORMAT` enum (`CLOCK_OFF`, `CLOCK_AMPM`, `CLOCK_24H`) and `homeScreenClock` member (default OFF)
- Added persistence in `writeSettings()` and `loadFromFile()` (appended at end for backward compatibility)
- Added "Home Screen Clock" setting under Display category in `SettingsList.h`
### Lyra Theme: Empty State Placeholder (`LyraTheme.cpp`)
- When `recentBooks` is empty, draws centered "Choose something to read" text instead of blank area
### Lyra Theme: 1-Book Horizontal Layout (`LyraTheme.cpp`)
- When 1 recent book: cover on the left (natural aspect ratio), title + author on the right
- Title uses `UI_12_FONT_ID` with generous wrapping (up to 5 lines, no truncation unless very long)
- Author in `UI_10_FONT_ID` below title with 4px gap
### Lyra Theme: Multi-Book Tile Layout (`LyraTheme.cpp`)
- 2-3 books: tile-based layout with cover centered within tile (no stretching)
- Cover bitmap rendering preserves aspect ratio: crops if wider than slot, centers if narrower
- Title wraps to 2 lines with ellipsis, author in `SMALL_FONT_ID` below
### Lyra Theme: Selection Background Fix (`LyraTheme.cpp`)
- Bottom section of selection highlight now uses full remaining height below cover
- Prevents author text from clipping outside the selection area
### Lyra Theme: Shared Helpers (`LyraTheme.cpp`)
- Extracted `wrapText` lambda for reusable word-wrap logic (parameterized by font, maxLines, maxWidth)
- Extracted `renderCoverBitmap` lambda for aspect-ratio-preserving cover rendering
### Lyra Metrics (`LyraTheme.h`)
- Increased `homeCoverTileHeight` from 287 to 310 to accommodate expanded text area
- Menu buttons shift down automatically since they're positioned relative to this metric
### Home Screen Clock (`HomeActivity.cpp`)
- Added clock rendering in the header area (top-left) after `drawHeader()`
- Respects `homeScreenClock` setting (OFF / AM/PM / 24H)
- Skips rendering if system time is unset (year <= 2000)
### Set Time Activity (NEW: `SetTimeActivity.h`, `SetTimeActivity.cpp`)
- New sub-activity for manual time entry: displays HH:MM with field selection
- Left/Right switches between hours and minutes, Up/Down adjusts values
- Confirm saves via `settimeofday()`, Back discards
- Wired into Settings > Display as an action item
### Settings Activity Wiring (`SettingsActivity.h`, `SettingsActivity.cpp`)
- Added `SetTime` to `SettingAction` enum
- Added include and switch case for `SetTimeActivity`
- Added "Set Time" action to display settings category
## Build Verification
- `pio run -e default` succeeded
- RAM: 31.1% (101,772 / 327,680 bytes)
- Flash: 99.5%
## Follow-up Items
- Test on hardware: verify clock display, card layout with 0/1/2/3 books, Set Time activity
- Fine-tune `homeCoverTileHeight` value if text area feels too tight or loose visually
- Consider NTP auto-sync when WiFi is available (currently only during KOReader sync)

View File

@@ -0,0 +1,63 @@
# Upstream Sync: `upstream/master` into `mod/master`
**Date:** 2026-02-16
**Task:** Synchronize recent upstream changes into the mod fork while preserving all mod-specific features.
## Strategy
- **Cherry-pick** approach (not full merge) for granular control
- 13 upstream commits cherry-picked across 3 phases; 4 skipped (mod already had enhanced implementations)
- Working branch `mod/sync-upstream` used, then fast-forwarded into `mod/master`
## Phases & Commits
### Phase 1 — Low-risk (6 commits)
| PR | Description | Notes |
|----|-------------|-------|
| #689 | Hyphenation optimization | Naming conflict resolved (mod's `OMIT_HYPH_*` guards preserved) |
| #832 | Settings size auto-calc | Mod's `sleepScreenLetterboxFill` added to new `SettingsWriter` |
| #840 | clang-format-fix shebang | Clean |
| #917 | SCOPE.md dictionary docs | Clean |
| #856 | Multiple author display | Clean |
| #858 | Miniz compilation warning | Clean |
### Phase 2 — Major refactors (3 commits)
| PR | Description | Notes |
|----|-------------|-------|
| #774 | Activity render() refactor | ~15 conflicts. Mod's 5 custom activities (Bookmarks, Dictionary) adapted to new `render(RenderLock&&)` pattern. Deadlock in `EpubReaderActivity.cpp` identified and fixed (redundant mutex around `enterNewActivity()`). |
| #916 | RAII RenderLock | Clean cherry-pick + audit of mod code for manual mutex usage |
| #728 | I18n system | ~15-20 conflicts. 16 new `StrId` keys added for mod strings. `DynamicEnum` font handling preserved. `SettingsList.h` and `SettingsActivity.cpp` adapted. |
### Phase 3 — Post-I18n fixes (4 commits)
| PR | Description | Notes |
|----|-------------|-------|
| #884 | Empty button icon fix | Clean |
| #906 | Webserver docs update | Clean |
| #792 | Translators doc | Clean |
| #796 | Battery icon alignment | Clean |
### Mod adaptation commits (3)
- `mod: adapt mod activities to #774 render() pattern` — Systematic refactor of 5 activity pairs
- `mod: convert remaining manual render locks to RAII RenderLock` — Audit & cleanup
- `mod: remove duplicate I18n.h include in HomeActivity.cpp` — Cleanup
## Skipped Upstream Commits (4)
- #676, #700 (image/cover improvements) — Mod already has enhanced pipeline
- #668 (JPEG support) — Already in mod
- #780 (cover fallback) — Already cherry-picked into mod
## Files Changed
111 files changed, ~23,700 insertions, ~17,700 deletions across hyphenation tries, I18n system, activity refactors, and documentation.
## Key Decisions
- **Image pipeline:** Kept mod's versions entirely; upstream's cover fixes were already ported
- **Deadlock fix:** Removed redundant `xSemaphoreTake`/`xSemaphoreGive` around `enterNewActivity()` in `EpubReaderActivity.cpp` — the new `RenderLock` inside `enterNewActivity()` would deadlock with the outer manual lock
- **I18n integration:** Added mod-specific `StrId` keys rather than keeping hardcoded strings
## Verification
- All mod features (bookmarks, dictionary, letterbox fill, placeholder covers, table rendering, embedded images) verified for code-path integrity post-sync
- No broken references or missing dependencies found
## Follow-up Items
- Full PlatformIO build on hardware to confirm compilation
- Runtime testing of all mod features on device

View File

@@ -0,0 +1,38 @@
# Clock Persistence, Size Setting, and Timezone Support
## Task Description
Implemented the plan from `clock_settings_and_timezone_fd0bf03f.plan.md` covering three features:
1. Fix homeScreenClock setting not persisting across reboots
2. Add clock size setting (Small/Medium/Large)
3. Add timezone selection with North American presets and custom UTC offset
## Changes Made
### 1. Fix Persistence Bug
- **`src/CrossPointSettings.cpp`**: Removed stale legacy `sleepScreenGradientDir` read (lines 270-271) from `loadFromFile()` that was causing a one-byte deserialization offset, corrupting `preferredPortrait`, `preferredLandscape`, and `homeScreenClock`.
### 2. Clock Size Setting
- **`src/CrossPointSettings.h`**: Added `CLOCK_SIZE` enum (SMALL/MEDIUM/LARGE) and `uint8_t clockSize` field.
- **`src/CrossPointSettings.cpp`**: Added `clockSize` to `writeSettings()` and `loadFromFile()` serialization.
- **`src/SettingsList.h`**: Added clock size enum setting in Display category.
- **`lib/I18n/I18nKeys.h`**: Added `STR_CLOCK_SIZE`, `STR_CLOCK_SIZE_SMALL`, `STR_CLOCK_SIZE_MEDIUM`, `STR_CLOCK_SIZE_LARGE`.
- **All 8 YAML translation files**: Added clock size strings.
- **`src/components/themes/BaseTheme.cpp`** and **`src/components/themes/lyra/LyraTheme.cpp`**: Updated `drawHeader()` to select font (SMALL_FONT_ID / UI_10_FONT_ID / UI_12_FONT_ID) based on `clockSize` setting.
### 3. Timezone Support
- **`src/CrossPointSettings.h`**: Added `TIMEZONE` enum (UTC, Eastern, Central, Mountain, Pacific, Alaska, Hawaii, Custom), `uint8_t timezone` and `int8_t timezoneOffsetHours` fields, and `getTimezonePosixStr()` declaration.
- **`src/CrossPointSettings.cpp`**: Added timezone/offset serialization, validation (-12 to +14), and `getTimezonePosixStr()` implementation returning POSIX TZ strings (including DST rules for NA timezones).
- **`lib/I18n/I18nKeys.h`** + **all YAML files**: Added timezone strings and "Set UTC Offset" label.
- **`src/SettingsList.h`**: Added timezone enum setting in Display category.
- **`src/activities/settings/SetTimezoneOffsetActivity.h/.cpp`** (new files): UTC offset picker activity (-12 to +14), using same UI pattern as `SetTimeActivity`.
- **`src/activities/settings/SettingsActivity.h`**: Added `SetTimezoneOffset` to `SettingAction` enum.
- **`src/activities/settings/SettingsActivity.cpp`**: Added include, action entry, handler for SetTimezoneOffset, and `setenv`/`tzset` call after every settings save.
- **`src/main.cpp`**: Apply saved timezone via `setenv`/`tzset` on boot after `SETTINGS.loadFromFile()`.
- **`src/util/TimeSync.cpp`**: Apply timezone before starting NTP sync so time displays correctly.
## Build Status
Firmware builds successfully (99.5% flash usage).
## Follow-up Items
- Test on device: verify clock persistence, size changes, timezone selection, and custom UTC offset picker.
- Timezone strings use English fallbacks for non-English languages.

View File

@@ -0,0 +1,33 @@
# Move Clock Settings to Own Category
## Task Description
Moved all clock-related settings into a dedicated "Clock" tab in the settings screen, renamed "Home Screen Clock" to "Clock" (since it's now shown globally), and made "Set UTC Offset" conditionally visible only when timezone is set to "Custom".
## Changes Made
### 1. Rename "Home Screen Clock" -> "Clock"
- **`lib/I18n/I18nKeys.h`**: Renamed `STR_HOME_SCREEN_CLOCK` to `STR_CLOCK`.
- **All 8 YAML translation files**: Updated key name and shortened labels (e.g., "Home Screen Clock" -> "Clock", "Hodiny na domovske obrazovce" -> "Hodiny").
- **`src/CrossPointSettings.h`**: Renamed field `homeScreenClock` to `clockFormat`.
- **`src/CrossPointSettings.cpp`**: Updated all references (`writeSettings`, `loadFromFile`).
- **`src/SettingsList.h`**: Updated setting entry and JSON API key.
- **`src/main.cpp`**, **`src/components/themes/BaseTheme.cpp`**, **`src/components/themes/lyra/LyraTheme.cpp`**: Updated all `SETTINGS.homeScreenClock` references to `SETTINGS.clockFormat`.
### 2. New "Clock" settings category
- **`lib/I18n/I18nKeys.h`**: Added `STR_CAT_CLOCK`.
- **All 8 YAML translation files**: Added localized "Clock" category label.
- **`src/SettingsList.h`**: Changed category of Clock, Clock Size, and Timezone from `STR_CAT_DISPLAY` to `STR_CAT_CLOCK`.
### 3. Wire up in SettingsActivity
- **`src/activities/settings/SettingsActivity.h`**: Added `clockSettings` vector, `rebuildClockActions()` helper, bumped `categoryCount` to 5.
- **`src/activities/settings/SettingsActivity.cpp`**:
- Added `STR_CAT_CLOCK` to `categoryNames` (index 1, between Display and Reader).
- Added routing for `STR_CAT_CLOCK` in the category-sorting loop.
- Implemented `rebuildClockActions()`: always adds "Set Time", conditionally adds "Set UTC Offset" only when `timezone == TZ_CUSTOM`. Called on `onEnter()` and after every `toggleCurrentSetting()`.
- Updated category switch indices (Display=0, Clock=1, Reader=2, Controls=3, System=4).
## Build Status
Firmware builds successfully.
## Follow-up Items
- Test on device: verify the new Clock tab, setting visibility toggle, and tab bar layout with 5 tabs.

View File

@@ -0,0 +1,50 @@
# Clock Bug Fix, NTP Auto-Sync, and Sleep Persistence
**Date**: 2026-02-17
## Task Description
Three clock-related improvements:
1. Fix SetTimeActivity immediately dismissing when opened
2. Add automatic NTP time sync on WiFi connection
3. Verify time persistence across deep sleep modes
## Changes Made
### 1. Bug Fix: SetTimeActivity immediate dismiss (`src/activities/settings/SetTimeActivity.cpp`)
- **Root cause**: `loop()` used `wasReleased()` for Back, Confirm, Left, and Right buttons. The parent `SettingsActivity` enters this subactivity on a `wasPressed()` event, so the button release from the original press immediately triggered the exit path.
- **Fix**: Changed all four `wasReleased()` calls to `wasPressed()`, matching the pattern used by all other subactivities (e.g., `LanguageSelectActivity`).
### 2. Shared NTP Utility (`src/util/TimeSync.h`, `src/util/TimeSync.cpp`)
- Created `TimeSync` namespace with three functions:
- `startNtpSync()` -- non-blocking: configures and starts SNTP service
- `waitForNtpSync(int timeoutMs = 5000)` -- blocking: starts SNTP and polls until sync completes or timeout
- `stopNtpSync()` -- stops the SNTP service
### 3. NTP on WiFi Connection (`src/activities/network/WifiSelectionActivity.cpp`)
- Added `#include "util/TimeSync.h"` and call to `TimeSync::startNtpSync()` in `checkConnectionStatus()` right after `WL_CONNECTED` is detected. This is non-blocking so it doesn't delay the UI flow.
### 4. KOReaderSync refactor (`src/activities/reader/KOReaderSyncActivity.cpp`)
- Removed the local `syncTimeWithNTP()` anonymous namespace function and `#include <esp_sntp.h>`
- Replaced both call sites with `TimeSync::waitForNtpSync()` (blocking, since KOReader needs accurate time for API requests)
- Added `#include "util/TimeSync.h"`
### 5. Boot time debug log (`src/main.cpp`)
- Added `#include <ctime>` and a debug log in `setup()` that prints the current RTC time on boot (or "not set" if epoch). This helps verify time persistence across deep sleep during testing.
## Sleep Persistence Notes
- ESP32-C3's default ESP-IDF config uses both RTC and high-resolution timers for timekeeping
- The RTC timer continues during deep sleep, so `time()` / `gettimeofday()` return correct wall-clock time after wake (with drift from the internal ~150kHz RC oscillator)
- Time does NOT survive a full power-on reset (RTC timer resets)
- NTP auto-sync on WiFi connection handles drift correction
## Build Verification
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
## Follow-up Items
- Test on hardware: set time manually, sleep, wake, verify time in serial log
- Test NTP: connect to WiFi from Settings, verify time updates automatically
- Consider adding `TimeSync::stopNtpSync()` call when WiFi is disconnected (currently SNTP just stops getting responses, which is harmless)

View File

@@ -0,0 +1,47 @@
# Clock UI: Symmetry, Auto-Update, and System-Wide Header Clock
**Date**: 2026-02-17
## Task Description
Three improvements to clock display:
1. Make clock positioning symmetric with battery icon in headers
2. Auto-update the clock without requiring a button press
3. Show the clock everywhere the battery appears in system UI headers
## Changes Made
### 1. Moved clock rendering into `drawHeader` (BaseTheme + LyraTheme)
Previously the clock was drawn only in `HomeActivity::render()` with ad-hoc positioning (`contentSidePadding`, `topPadding`). Now it's rendered inside `drawHeader()` in both theme implementations, using the same positioning pattern as the battery:
- **Battery** (right): icon at `rect.x + rect.width - 12 - batteryWidth`, text at `rect.y + 5`
- **Clock** (left): text at `rect.x + 12`, `rect.y + 5`
Both use `SMALL_FONT_ID`, so the font matches. The 12px margin from the edge is now symmetric on both sides.
**Files changed:**
- `src/components/themes/BaseTheme.cpp` -- added clock block in `drawHeader()`, added `#include <ctime>` and `#include "CrossPointSettings.h"`
- `src/components/themes/lyra/LyraTheme.cpp` -- same changes for Lyra theme's `drawHeader()`
### 2. Clock now appears on all header screens automatically
Since `drawHeader()` is called by: HomeActivity, MyLibraryActivity, RecentBooksActivity, SettingsActivity, LookedUpWordsActivity, DictionarySuggestionsActivity -- the clock now appears on all of these screens when enabled. No per-activity code needed.
### 3. Removed standalone clock code from HomeActivity
- `src/activities/home/HomeActivity.cpp` -- removed the 15-line clock rendering block that was separate from `drawHeader()`
### 4. Added auto-update (once per minute) on home screen
- `src/activities/home/HomeActivity.h` -- added `lastRenderedMinute` field to track the currently displayed minute
- `src/activities/home/HomeActivity.cpp` -- added minute-change detection in `loop()` that calls `requestUpdate()` when the minute rolls over, triggering a screen refresh
## Build Verification
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
## Follow-up Items
- The auto-update only applies to HomeActivity. Other screens (Settings, Library, etc.) will show the current time when they render but won't auto-refresh purely for clock updates, which is appropriate for e-ink.
- The DictionarySuggestionsActivity and LookedUpWordsActivity pass a non-zero `contentX` offset in their header rect, so the clock position adjusts correctly via `rect.x + 12`.

View File

@@ -0,0 +1,24 @@
# Home Screen Book Card Highlight Fixes
## Task
Fix visual issues with the selection highlight on home screen recent book cards:
1. Bottom padding too tight against author text descenders
2. Rounded corners not uniform across all four corners
## Changes Made
### LyraTheme.h
- Increased `homeCoverTileHeight` from 310 to 318 to provide bottom padding below author text
### LyraTheme.cpp
- Reduced `cornerRadius` from 6 to 4 to prevent radius clamping in the 8px-tall top strip (which was clamping `min(6, w/2, 4) = 4` while the bottom section used the full 6)
### GfxRenderer.cpp
- No net changes. Two approaches were tried and reverted:
1. **Dither origin** (global shift): misaligned the highlight's dot pattern with surrounding UI
2. **Arc-relative dither** (per-corner relative coords): created visible seam artifacts at 3 of 4 corners where the arc's relative dither met the adjacent rectangle's absolute dither with inverted parity
## Key Decisions
- **Bottom padding**: Originally tried shrinking the highlight (subtracting extra padding from bottom section height), but this cut off the author text. Correct approach: increase the tile height so the highlight naturally has more room.
- **Corner radius**: Reducing from 6 to 4 ensures all corners (both the thin top strip and taller bottom section) use the same `maxRadius` through `fillRoundedRect`'s clamping formula.
- **Dither approaches (both reverted)**: Two dither fixes were tried: (1) global dither origin shift misaligned with surrounding UI, (2) arc-relative dither created visible seam artifacts at corners where parity mismatched. The absolute dither pattern in `fillArc` is the least-bad option — the minor L/R asymmetry at radius 4 is much less noticeable than seam artifacts.

View File

@@ -0,0 +1,37 @@
# Long-Press Confirm to Open Table of Contents
**Date**: 2026-02-17
**Branch**: mod/improve-home-screen
## Task
Add long-press detection on the Confirm button while reading an EPUB to directly open the Table of Contents (chapter selection), bypassing the reader menu. Short press retains existing behavior (opens menu).
## Changes Made
### `src/activities/reader/EpubReaderActivity.h`
- Added `bool ignoreNextConfirmRelease` member to suppress short-press after a long-press Confirm
- Added `void openChapterSelection()` private method declaration
### `src/activities/reader/EpubReaderActivity.cpp`
- Added `constexpr unsigned long longPressConfirmMs = 700` threshold constant
- Extracted `openChapterSelection()` helper method from the duplicated `EpubReaderChapterSelectionActivity` construction code
- Added long-press Confirm detection in `loop()` (before the existing short-press check): opens TOC directly if `epub->getTocItemsCount() > 0`
- Refactored `onReaderMenuConfirm(SELECT_CHAPTER)` to use the new helper (was ~35 lines of inline construction)
- Refactored `onReaderMenuConfirm(GO_TO_BOOKMARK)` fallback (no bookmarks + TOC available) to use the same helper
- Reset `ignoreNextConfirmRelease` when `skipNextButtonCheck` clears, to avoid stale state across subactivity transitions
### `src/activities/reader/EpubReaderChapterSelectionActivity.h`
- Added `bool ignoreNextConfirmRelease` member
- Added `initialSkipRelease` constructor parameter (default `false`) to consume stale Confirm release when opened via long-press
### `src/activities/reader/EpubReaderChapterSelectionActivity.cpp`
- Added guard in `loop()` to skip the first Confirm release when `ignoreNextConfirmRelease` is true
## Pattern Used
Follows the existing Back button short/long-press pattern: `isPressed() + getHeldTime() >= threshold` for long press, `wasReleased()` for short press, with `ignoreNextConfirmRelease` flag (same pattern as `EpubReaderMenuActivity`, `LookedUpWordsActivity`, and other activities).
## Follow-up Items
- None identified

View File

@@ -0,0 +1,17 @@
# Cherry-pick upstream PR #939 — dangling pointer fix
## Task
Cherry-pick commit `b47e1f6` from upstream PR [#939](https://github.com/crosspoint-reader/crosspoint-reader/pull/939) into `mod/master`.
## Changes
- **File**: `src/activities/home/MyLibraryActivity.cpp` (lines 199-200)
- **Fix**: Changed `folderName` from `auto` (deduced as `const char*` pointing to a temporary) to `std::string`, and called `.c_str()` at the point of use instead. This eliminates a dangling pointer caused by `.c_str()` on a temporary `std::string` from `basepath.substr(...)`.
## Method
- Fetched PR ref via `git fetch upstream pull/939/head:pr-939`
- Cherry-picked `b47e1f6` — applied cleanly with no conflicts
- Build verified: SUCCESS (PlatformIO, 68s)
- Cleaned up temporary `pr-939` branch ref
## Follow-up
- None required. The commit preserves original authorship (Uri Tauber).

View File

@@ -0,0 +1,42 @@
# Fix Indexing Display Issues
**Date:** 2026-02-18
**Branch:** `mod/merge-upstream-pr-979`
## Task
Fixed three issues with the indexing display implementation that was added as part of merging PR #979 (silent pre-indexing for next chapter):
1. **Restore original popup for direct chapter jumps** — The conditional logic that showed a full-screen "Indexing..." message when a status bar display mode was selected was removed. Direct chapter jumps now always show the original small popup overlay, regardless of the Indexing Display setting.
2. **Clear status bar indicator after silent indexing** — Added `preIndexedNextSpine` tracking member to prevent the `silentIndexingActive` flag from being re-set on re-renders after indexing completes. Changed `silentIndexNextChapterIfNeeded` to return `bool` and added `requestUpdate()` call to trigger a clean re-render that clears the indicator.
3. **Handle single-page chapters** — Updated the pre-indexing condition to trigger on the sole page of a 1-page chapter (not just the penultimate page of multi-page chapters).
## Files Changed
- `src/activities/reader/EpubReaderActivity.h` — Added `preIndexedNextSpine` member, changed `silentIndexNextChapterIfNeeded` return type to `bool`
- `src/activities/reader/EpubReaderActivity.cpp` — All three fixes applied
## Build
PlatformIO build succeeded (RAM: 31.1%, Flash: 99.6%).
## Follow-up fix: False "Indexing" indicator + image flash on e-ink
Two related issues: (1) paging backwards to a penultimate page of an already-indexed chapter showed "Indexing" in the status bar, and (2) the `requestUpdate()` that cleared the indicator caused images to flash on e-ink.
Root cause: `silentIndexingActive` was set optimistically based on page position alone, before checking whether the next chapter's cache actually exists. The subsequent `requestUpdate()` to clear the indicator triggered a full re-render causing image artifacts.
Fix — replaced optimistic flag with a pre-check:
- Before rendering, probes the next chapter's section file via `Section::loadSectionFile`. If cached, sets `preIndexedNextSpine` and leaves `silentIndexingActive = false`. Only sets `true` when indexing is genuinely needed.
- Removed `requestUpdate()` entirely — the indicator clears naturally on the next page turn.
- Added early-out in `silentIndexNextChapterIfNeeded` for `preIndexedNextSpine` match to avoid redundant Section construction.
The pre-check cost (one `loadSectionFile` call) only happens once per chapter due to `preIndexedNextSpine` caching.
Silent indexing is only performed on text-only penultimate pages (`!p->hasImages()`). On image pages, silent indexing is skipped entirely — the normal popup handles indexing on the next chapter transition. This avoids conflicts with the grayscale rendering pipeline (`displayWindow` after `displayGrayBuffer` triggers `grayscaleRevert`, causing image inversion/ghosting).
For text-only pages: after `renderContents` returns, the indicator is cleared via `displayWindow(FAST_REFRESH)` on just the status bar strip. This is safe because text-only pages use simple BW rendering without the grayscale pipeline.
Build succeeded (RAM: 31.1%, Flash: 99.6%).

View File

@@ -0,0 +1,39 @@
# Merge PR #979: Silent Pre-Indexing + Indexing Display Setting
**Date:** 2026-02-18
**Branch:** `mod/merge-upstream-pr-979`
## Task Description
Merged upstream PR #979 (silent pre-indexing for the next chapter) and implemented both "Possible Improvements" from the PR:
1. A user setting to choose between Popup, Status Bar Text, or Status Bar Icon for indexing feedback.
2. Status bar indicator rendering during silent pre-indexing.
## Changes Made
### PR #979 Cherry-Pick (2 files)
- **`src/activities/reader/EpubReaderActivity.h`** -- Added `silentIndexNextChapterIfNeeded()` method declaration and `silentIndexingActive` flag.
- **`src/activities/reader/EpubReaderActivity.cpp`** -- Moved viewport dimension calculations before the `!section` block. Added `silentIndexNextChapterIfNeeded()` implementation that pre-indexes the next chapter when the penultimate page is displayed.
### New "Indexing Display" Setting (5 files)
- **`src/CrossPointSettings.h`** -- Added `INDEXING_DISPLAY` enum (POPUP, STATUS_TEXT, STATUS_ICON) and `indexingDisplay` field.
- **`src/CrossPointSettings.cpp`** -- Added persistence (write/read) for the new setting at the end of the settings chain.
- **`src/SettingsList.h`** -- Registered the new Enum setting in the Display category.
- **`lib/I18n/I18nKeys.h`** -- Added 4 new string IDs: `STR_INDEXING_DISPLAY`, `STR_INDEXING_POPUP`, `STR_INDEXING_STATUS_TEXT`, `STR_INDEXING_STATUS_ICON`.
- **`lib/I18n/translations/*.yaml`** (8 files) -- Added translations for all 8 languages.
### Status Bar Indicator Implementation
- **`EpubReaderActivity.cpp`** -- Conditional popup logic: only shows popup when setting is POPUP; for STATUS_TEXT/STATUS_ICON, shows centered "Indexing..." text on blank screen during non-silent indexing. For silent pre-indexing, sets `silentIndexingActive` flag before rendering so `renderStatusBar()` draws the indicator (text or 8x8 hourglass icon) in the status bar.
- Defined an 8x8 1-bit hourglass bitmap icon (`kIndexingIcon`) for the icon mode.
## Build Verification
- PlatformIO build: SUCCESS
- RAM: 31.1% (101,820 / 327,680 bytes)
- Flash: 99.6% (6,525,028 / 6,553,600 bytes)
## Follow-Up Items
- Test on device: verify silent indexing triggers on penultimate page, verify all three display modes work correctly.
- The status bar indicator shows optimistically on penultimate pages even if the next chapter is already cached (false positive clears immediately after `loadSectionFile` check).
- Flash usage is at 99.6% -- monitor for future additions.

View File

@@ -0,0 +1,59 @@
# Merge Upstream PRs #965, #939, #852, #972, #971, #977, #975
**Date:** 2026-02-18
**Branch:** mod/merge-upstream-1
## Task
Port 7 upstream PRs from crosspoint-reader/crosspoint-reader into the mod branch.
## Status per PR
| PR | Description | Result |
|---|---|---|
| #939 | Fix dangling pointer in MyLibraryActivity | Already ported, no changes needed |
| #852 | HalPowerManager idle CPU freq scaling | Completed partial port (Lock RAII, WiFi check, skipLoopDelay, render locks) |
| #965 | Fix paragraph formatting inside list items | Fully ported |
| #972 | Micro-optimizations to eliminate value copies | Ported (fontMap move, getDataFromBook const ref) |
| #971 | Remove redundant hasPrintableChars pass | Fully ported |
| #977 | Skip unsupported image formats during parsing | Fully ported |
| #975 | Fix UITheme memory leak on theme reload | Fully ported |
## Changes Made
### PR #852 (partial port completion)
- `lib/hal/HalPowerManager.h` -- Added `LockMode` enum, `currentLockMode`/`modeMutex` members, nested `Lock` RAII class (non-copyable/non-movable), `extern powerManager` declaration
- `lib/hal/HalPowerManager.cpp` -- Added mutex init in `begin()`, WiFi.getMode() check in `setPowerSaving()`, Lock constructor/destructor, LockMode guard
- `src/activities/settings/ClearCacheActivity.h` -- Added `skipLoopDelay()` override
- `src/activities/settings/OtaUpdateActivity.h` -- Added `skipLoopDelay()` override
- `src/activities/Activity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
- `src/activities/ActivityWithSubactivity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
### PR #965
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` -- Added `listItemUntilDepth` member
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Modified `startElement` to handle `<p>` inside `<li>` without line break; added depth reset in `endElement`
### PR #972
- `lib/GfxRenderer/GfxRenderer.cpp` -- `std::move(font)` in `insertFont`
- `src/RecentBooksStore.h` / `.cpp` -- `getDataFromBook` now takes `const std::string&`
### PR #971
- `lib/EpdFont/EpdFont.h` / `.cpp` -- Removed `hasPrintableChars` method
- `lib/EpdFont/EpdFontFamily.h` / `.cpp` -- Removed `hasPrintableChars` method
- `lib/GfxRenderer/GfxRenderer.cpp` -- Removed 3 early-return guards calling `hasPrintableChars`
### PR #977
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Added `PARSE_BUFFER_SIZE` constant, `isFormatSupported` guard before image extraction, timing instrumentation
### PR #975
- `src/components/UITheme.h` -- Changed `currentTheme` to `std::unique_ptr<const BaseTheme>`
- `src/components/UITheme.cpp` -- Changed allocations to `std::make_unique`
## Build Result
Build succeeded: RAM 31.1%, Flash 99.5%
## Follow-up Items
- PR #972 LyraTheme loop variable change not applicable (our code uses index-based loops)
- Test on device to verify all changes work as expected

View File

@@ -0,0 +1,49 @@
# Sync mod/master with upstream 1.1.0-RC
## Task Description
Integrated 19 missing upstream PRs from `master` (1.1.0-RC) into `mod/master` via phased cherry-picking, preserving all existing mod enhancements.
## Changes Made
### Branch Setup
- Created safety branch `mod/backup-pre-sync` from `mod/master`
- Created integration branch `mod/sync-upstream-1.1.0` (19 cherry-picked commits + 1 cleanup)
### Phase 1: Low-Risk PRs (8 PRs)
- **#646** Ukrainian hyphenation support (resolved conflict: merged with mod's `OMIT_HYPH_*` conditional compilation guards)
- **#732** Lyra screens (resolved 5 conflicts: preserved mod's clock, 3-cover layout, cover rendering; added upstream's drawSubHeader, popup, keyboard/text field methods)
- **#725** Lyra Icons (resolved conflicts: added icon infrastructure, iconForName(), Lyra icon assets; removed stale Lyra3CoversTheme.cpp)
- **#768** Tweak Lyra popup UI (resolved conflicts: preserved mod's epub loading logic, added upstream popup constants)
- **#880** Flash objects listing script (clean)
- **#897** Keyboard font size increase (clean)
- **#927** Translators list update (clean)
- **#935** Missing up/down button labels (resolved conflicts: kept GUI.drawList() over old manual rendering)
### Phase 2: Medium-Risk PRs (8 PRs - all applied cleanly)
- **#783** KOSync repositioning fix
- **#923** Bresenham line drawing
- **#933** Font map lookup performance
- **#944** 4-bit BMP support
- **#952** Skip large CSS files to prevent crashes
- **#963** Word width and space calculation fix
- **#964** Scale cover images up
- **#970** Fix prev-page teleport to end of book
### Phase 3: Large PR
- **#831** Compressed fonts (30.7% flash reduction, 73 files). Resolved 2 conflicts: merged font decompressor init in main.cpp, merged clearFontCache with mod's silent indexing in EpubReaderActivity.
### Phase 4: Overlap Assessment
- **#556** (JPEG/PNG image support): Confirmed mod already has complete coverage. All converter files identical. No action needed.
- **#980** (Basic table support): Confirmed mod's column-aligned table rendering is a strict superset. No action needed.
### Post-Sync
- Removed stale `Lyra3CoversTheme.h` (3-cover support merged into LyraTheme)
- Fixed `UITheme.cpp` to use `LyraTheme` for LYRA_3_COVERS variant
- Updated `open-x4-sdk` submodule to `91e7e2b` (drawImageTransparent support for Lyra Icons)
- Ran clang-format on all source files
- Build verified: RAM 31.4%, Flash 57.0%
## Follow-Up Items
- Merge `mod/sync-upstream-1.1.0` into `mod/master` when ready
- On-device smoke testing (book loading, images, tables, bookmarks, dictionary, sleep screen, clock, home screen)
- Safety branch `mod/backup-pre-sync` available for rollback if needed

View File

@@ -0,0 +1,33 @@
# Port Upstream PRs #997, #1003, #1005, #1010
## Task
Cherry-pick / port four upstream PRs from crosspoint-reader/crosspoint-reader into the mod fork.
## Changes Made
### PR #997 -- Already Ported (no changes)
Glyph null-safety in `getSpaceWidth`/`getTextAdvanceX` was already present via commit `c1b8e53`.
### PR #1010 -- Fix Dangling Pointer
- **src/main.cpp**: `onGoToReader()` now copies the `initialEpubPath` string before calling `exitActivity()`, preventing a dangling reference when the owning activity is destroyed.
### PR #1005 -- Use HalPowerManager for Battery Percentage
- **lib/hal/HalPowerManager.h**: Changed `getBatteryPercentage()` return type from `int` to `uint16_t`.
- **lib/hal/HalPowerManager.cpp**: Same return type change.
- **src/Battery.h**: Emptied contents (was `static BatteryMonitor battery(BAT_GPIO0)`).
- **src/main.cpp**: Removed `#include "Battery.h"`.
- **src/activities/home/HomeActivity.cpp**: Removed `#include "Battery.h"`.
- **src/components/themes/BaseTheme.cpp**: Replaced `Battery.h` include with `HalPowerManager.h`, replaced `battery.readPercentage()` with `powerManager.getBatteryPercentage()` (2 occurrences).
- **src/components/themes/lyra/LyraTheme.cpp**: Same replacements (2 occurrences).
### PR #1003 -- Render Image Placeholders While Waiting for Decode
- **lib/Epub/Epub/blocks/ImageBlock.h/.cpp**: Added `isCached()` method that checks if the `.pxc` cache file exists.
- **lib/Epub/Epub/Page.h/.cpp**: Added `PageImage::isCached()`, `PageImage::renderPlaceholder()`, `Page::renderTextOnly()`, `Page::countUncachedImages()`, `Page::renderImagePlaceholders()`. Added `#include <GfxRenderer.h>` to Page.h.
- **src/activities/reader/EpubReaderActivity.cpp**: Modified `renderContents()` to check for uncached images and display text + placeholder rectangles immediately (Phase 1 with HALF_REFRESH), then proceed with full image decode and display (Phase 2 with fast refresh). Existing mod-specific double FAST_REFRESH logic for anti-aliased image pages is preserved for the cached-image path.
## Build Result
SUCCESS -- RAM: 31.5%, Flash: 70.4%. No linter errors.
## Follow-up Items
- PR #1003 is still open upstream (not merged); may need to rebase if upstream changes before merge.
- The phased rendering for uncached images skips the mod's double FAST_REFRESH technique (relies on Phase 1's HALF_REFRESH instead). If grayscale quality on first-decode image pages is suboptimal, this could be revisited.

View File

@@ -0,0 +1,33 @@
# Port Upstream PR #978: Improve Font Drawing Performance
**Date:** 2026-02-19
**Branch:** `mod/more-upstream-patches-for-1.1.0`
**Commit:** `3a06418`
## Task
Port upstream PR #978 (perf: Improve font drawing performance) which optimizes glyph rendering by 15-23% on device.
## Changes Made
### Cherry-picked from upstream (commit `07d715e`)
- **`lib/GfxRenderer/GfxRenderer.cpp`**: Introduced `TextRotation` enum and `renderCharImpl<TextRotation>` template function that consolidates normal and 90-CW-rotated rendering paths. Hoists the `is2Bit` conditional above pixel loops (eliminating per-pixel branch). Uses `if constexpr` for compile-time rotation path selection. Fixes operator precedence bug in `bmpVal` calculation.
- **`lib/GfxRenderer/GfxRenderer.h`**: Changed `renderChar` signature (`const int* y` -> `int* y`). Moved `getGlyphBitmap` from private to public (needed by the free-function template).
### Mod-specific extension
- Extended `TextRotation` enum with `Rotated90CCW` and refactored `drawTextRotated90CCW` to delegate to the template. This fixed two bugs in the mod's CCW code:
1. Operator precedence bug in `bmpVal`: `3 - (byte >> bit_index) & 0x3` -> `3 - ((byte >> bit_index) & 0x3)`
2. Missing compressed font support: was using raw `bitmap[offset]` instead of `getGlyphBitmap()`
## Recovery of Discarded Changes
During plan-mode dry-run cherry-pick reset, `git checkout -- .` inadvertently discarded unstaged working tree changes in 12 files (from PRs #1005, #1010, #1003). These were recovered by:
- Cherry-picking upstream commits `cabbfcf` (#1005) and `63b2643` (#1010) with conflict resolution (kept mod's `CrossPointSettings.h` include)
- Fetching unmerged PR #1003 branch and manually applying the diff (ImageBlock::isCached, Page placeholder rendering, EpubReaderActivity phased rendering)
- Committed as `18be265`
## Build
PlatformIO build succeeded. RAM: 31.5%, Flash: 70.4%.

View File

@@ -0,0 +1,37 @@
# Port Upstream 1.1.0-RC Fixes
**Date:** 2026-02-19
**Task:** Port 3 commits from upstream crosspoint-reader PR #992 (1.1.0-rc branch) into mod/master.
## Commits Ported
### 1. chore: Bump version to 1.1.0 (402e887) — Skipped
- mod/master already has `version = 1.1.1-rc`, which is ahead of upstream's 1.1.0.
### 2. fix: Crash on unsupported bold/italic glyphs (3e2c518)
- **File:** `lib/GfxRenderer/GfxRenderer.cpp`
- Added null-safety checks to `getSpaceWidth()` and `getTextAdvanceX()`.
- `getSpaceWidth()`: returns 0 if the space glyph is missing in the styled font variant.
- `getTextAdvanceX()`: falls back to `REPLACEMENT_GLYPH` (U+FFFD) if a glyph is missing, and treats as zero-width if the replacement is also unavailable.
- Prevents a RISC-V Load access fault (nullptr dereference) when indexing chapters with characters unsupported by bold/italic font variants.
### 3. fix: Increase PNGdec buffer for wide images (b8e743e)
- **Files:** `platformio.ini`, `lib/Epub/Epub/converters/PngToFramebufferConverter.cpp`
- Bumped `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 (supports up to 2048px wide RGBA images).
- Added `bytesPerPixelFromType()` and `requiredPngInternalBufferBytes()` helper functions.
- Added a pre-decode safety check that aborts with a log error if the PNG scanline buffer would overflow PNGdec's internal buffer.
## Verification
- Build (`pio run -e default`) succeeded cleanly with no errors or warnings.
- RAM: 31.5%, Flash: 70.4%.
## 4. CSS-aware image sizing (PR #1002, commit c8ba4fe)
- **Files:** `lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`, `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`
- Added `imageHeight` field to `CssPropertyFlags` and `CssStyle` (our existing `width` field maps to upstream's `imageWidth`).
- Added CSS `height` property parsing into `imageHeight`.
- Added `imageHeight` and `width` to cache serialization; bumped `CSS_CACHE_VERSION` 2->3.
- Replaced viewport-fit-only image scaling in `ChapterHtmlSlimParser` with CSS-aware sizing: resolves CSS height/width (including inline styles), preserves aspect ratio, clamps to viewport, includes divide-by-zero guards.
- `platformio.ini` changes excluded from commit per user request (PNG buffer bump was already committed separately).
## Follow-up
- None required. Changes are straightforward upstream ports.

View File

@@ -0,0 +1,34 @@
# Sleep Cover: Double FAST_REFRESH for Dithered Letterbox
**Date:** 2026-02-19
## Task
Apply the "double FAST_REFRESH" technique (proven for inline EPUB images via PR #556) to sleep cover rendering when dithered letterboxing is enabled. This replaces the corruption-prone HALF_REFRESH with two FAST_REFRESH passes through a white intermediate state.
Also reverted the hash-based block dithering workaround back to standard Bayer dithering for all gray ranges, confirming the root cause was HALF_REFRESH rather than the dithering pattern.
## Changes Made
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Added `USE_SLEEP_DOUBLE_FAST_REFRESH` define** (set to 1): compile-time toggle for easy A/B testing.
- **Removed `bayerCrossesBwBoundary()` and `hashBlockDither()`**: These were the HALF_REFRESH crosstalk workaround (2x2 hash blocks for gray 171-254). Removed entirely since double FAST_REFRESH addresses the root cause.
- **Simplified `drawLetterboxFill()`**: Dithered mode now always uses `quantizeBayerDither()` for all gray ranges -- same algorithm used for non-boundary grays. No more hash/Bayer branching.
- **Modified `renderBitmapSleepScreen()` BW pass**: When dithered letterbox is active and `USE_SLEEP_DOUBLE_FAST_REFRESH` is enabled:
1. Clear screen to white + FAST_REFRESH (establishes clean baseline)
2. Render letterbox fill + cover + FAST_REFRESH (shows actual content)
- **Letterbox fill included in all greyscale passes**: Initially skipped to avoid scan coupling, but re-enabled after testing confirmed the double FAST_REFRESH baseline prevents corruption. This ensures the letterbox color matches the cover edge after the greyscale LUT is applied.
- **Standard HALF_REFRESH path preserved** for letterbox=none, letterbox=solid, or when define is disabled.
## Build
Successfully compiled with `pio run` (0 errors, 0 warnings).
## Testing
Confirmed working on-device -- dithered letterbox renders cleanly without corruption, and letterbox color matches the cover edge including after greyscale layer application.
## Commit
`0fda903` on branch `mod/sleep-screen-tweaks-on-1.1.0-rc-double-fast`

View File

@@ -0,0 +1,36 @@
# Port Upstream PRs #1038, #1045, #1037, #1019
## Task
Manually port 4 open upstream PRs into the `mod/sync-upstream-PRs` branch, pull in Romanian translations from `upstream/master`, and create a MERGED.md tracking document.
## Changes Made
### PR #1038 (partial -- incremental fixes)
- `lib/Epub/Epub/ParsedText.cpp`: Added `.erase()` calls in `layoutAndExtractLines` to remove consumed words after line extraction (fixes redundant early flush bug). Also fixed `wordContinues` flag handling in `hyphenateWordAtIndex` to preserve prefix's attach-to-previous flag.
### PR #1045 (direct string changes)
- Updated `STR_FORGET_BUTTON` in all 9 translation yaml files to shorter labels.
- Pulled `romanian.yaml` from `upstream/master` (merged upstream PR #987).
### PR #1037 (4-file manual port)
- `lib/Utf8/Utf8.h`: Added `utf8IsCombiningMark()` utility function.
- `lib/EpdFont/EpdFont.cpp`: Added combining mark positioning in `getTextBounds`.
- `lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`: Added NFC-like precomposition for common Western European diacritics in `collectCodepoints()`.
- `lib/GfxRenderer/GfxRenderer.cpp`: Added combining mark rendering in `drawText`, `drawTextRotated90CW`, `drawTextRotated90CCW` (mod-only), and `getTextAdvanceX`. Also wrapped cursor advance in `renderCharImpl` to skip combining marks.
### PR #1019 (1-file manual port)
- `src/activities/home/MyLibraryActivity.cpp`: Added `getFileExtension()` helper and updated `drawList` call to show file extensions in the File Browser.
### Tracking
- `mod/prs/MERGED.md`: Created tracking document with details for all 4 PRs.
## Build Verification
`pio run -e mod` succeeded (24.49s, Flash: 57.2%, RAM: 31.4%).
## Follow-up Items
- All 4 upstream PRs are still open; monitor for any review feedback or changes before they merge.
- Romanian yaml is missing mod-specific string translations (using English fallbacks).
- PR #1019 open question: how file extensions look on long filenames.

View File

@@ -0,0 +1,23 @@
# Expandable Selected Row for Long Filenames
## Task
Implement a mod enhancement to PR #1019 (Display file extensions in File Browser). When the selected row's filename is too long, expand that row to 2 lines with character-level text wrapping. The file extension moves to the bottom-right of the expanded area. Non-selected rows retain single-line truncation behavior.
## Changes Made
### `src/components/themes/BaseTheme.cpp`
- Added `wrapTextToLines` static helper in the anonymous namespace: character-level UTF-8-aware text wrapping with "..." truncation on the final line.
- Modified `drawList`: pre-loop expansion detection for the selected item, pagination adjustment (`pageItems - 1`), expanded selection highlight (`2 * rowHeight`), 2-line title rendering via `wrapTextToLines`, extension repositioned to bottom-right of expanded area, `yPos` tracking for subsequent items.
### `src/components/themes/lyra/LyraTheme.cpp`
- Added identical `wrapTextToLines` helper.
- Modified `drawList` with analogous expansion logic, adapted for Lyra-specific styling (rounded-rect selection highlight, icon support, scroll bar-aware content width, preliminary text width computation for expansion check).
### `mod/prs/MERGED.md`
- Updated PR #1019 section to document the mod enhancement, files modified, and design decisions.
## Follow-up Items
- Visual testing on device to verify text positioning and edge cases (very long single-word filenames, last item on page expanding, single-item lists).
- The page-up/page-down navigation in `MyLibraryActivity::loop()` uses the base `pageItems` from `getNumberOfItemsPerPage` which doesn't account for expansion. This causes a minor cosmetic mismatch (page jump size differs by 1 from visual page when expansion is active) but is functionally correct.

View File

@@ -0,0 +1,29 @@
# Fix Text Wrapping and Spacing for Expanded Selected Row
## Task
Improve the expandable selected row feature (from PR #1019 enhancement). Two issues were identified from device testing:
1. Character-level wrapping broke words mid-character (e.g., "Preside / nt"), resulting in unnatural line breaks.
2. Poor vertical spacing -- text lines clustered near the top of the expanded highlight area with large empty space at the bottom.
## Changes Made
### `src/components/themes/BaseTheme.cpp`
- Rewrote `wrapTextToLines` with 3-tier break logic:
1. Preferred delimiters: " -- ", " - ", en-dash, em-dash (breaks at last occurrence to maximize line 1)
2. Word boundaries: last space or hyphen that fits
3. Character-level fallback for long unbroken tokens
- Extracted `truncateWithEllipsis` helper to reduce duplication
- Fixed expanded row rendering: text lines vertically centered in 2x row height area, extension baseline-aligned with last text line
### `src/components/themes/lyra/LyraTheme.cpp`
- Same `wrapTextToLines` rewrite and `truncateWithEllipsis` helper
- Same vertical centering for expanded row text lines
- Icon also vertically centered in expanded area
- Extension baseline-aligned with last text line instead of fixed offset
### `mod/prs/MERGED.md`
- Updated PR #1019 mod enhancement section to reflect the new wrapping strategy and spacing improvements
## Follow-up Items
- Device testing to verify improved wrapping and spacing visually

View File

@@ -0,0 +1,51 @@
# Port Upstream 1.1.0 RC Commits to mod/master
## Task Description
Audited all 11 commits from upstream PR #992 (1.1.0 Release Candidate) and ported the remaining unported/partially-ported ones to `mod/master`.
## Commit Audit Results
### Already Fully Ported (no changes needed)
- **402e887** - Bump version to 1.1.0 (skip, not relevant)
- **3e2c518** (#997) - Glyph null-safety (ported in c1b8e53)
- **b8e743e** (#995) - PNGdec buffer size (ported in c1b8e53)
- **588984e** (#1010) - Dangling pointer (ported in 18be265)
- **87d9d1d** (#978) - Font drawing performance (ported in 3a06418)
- **2cc497c** (#957) - Double FAST_REFRESH (ported in ad282ca)
### Already Effectively Present
- **8db3542** (#1017) - Cover outlines for Lyra themes (mod's LyraTheme.cpp already draws outlines unconditionally; Lyra3CoversTheme.cpp doesn't exist in mod)
### Ported in This Session
#### 1. Aligned #1002 + Ported #1018 (CSS cache invalidation)
**Files:** `CssParser.h`, `CssParser.cpp`, `Epub.cpp`, `ChapterHtmlSlimParser.cpp`
- Added `tryInterpretLength()` (bool return + out-param) to properly skip non-length CSS values like `auto`, `inherit`
- Added `deleteCache()` method to CssParser
- Moved `CSS_CACHE_VERSION` to static class member
- Added stale cache file removal in `loadFromCache()` on version mismatch
- Added "both CSS width and height set" branch in image sizing logic
- Refactored `parseCssFiles()` to early-return when cache exists
- Refactored `load()` to call `loadFromCache()` and invalidate sections on stale cache
#### 2. Ported #1014 (Strip unused CSS rules)
**File:** `CssParser.cpp`
- Added selector filtering in `processRuleBlockWithStyle` to skip `+`, `>`, `[`, `:`, `#`, `~`, `*`, and whitespace selectors
- Fixed `normalized()` trailing whitespace to use `while` loop and also strip `\n`
- Added TODO comments for multi-class selector support
#### 3. Ported #990 (Continue reading card classic theme)
**Files:** `BaseTheme.h`, `BaseTheme.cpp`
- Changed `homeTopPadding` from 20 to 40
- Computed `bookWidth` from cover BMP aspect ratio (clamped to 90% screen width, fallback to half-width)
- Fixed centering: added `rect.x` offset to `bookX`, `boxX`, and `continueBoxX`
- Simplified cover drawing (removed scaling/centering math since bookWidth now matches aspect ratio)
## Build Status
All changes compile successfully (0 errors, 0 warnings in modified files).
## Follow-up Items
- Test on device to verify CSS cache invalidation works correctly (books with stale caches should auto-rebuild)
- Test classic theme continue reading card with various cover aspect ratios
- Test image sizing with EPUBs that specify both CSS width and height on images

View File

@@ -0,0 +1,27 @@
# NTP Sync Clock Feature
## Task
Add a "Sync Clock" action to the Clock settings category that connects to WiFi and performs NTP time synchronization.
## Changes Made
### New Files
- **src/activities/settings/NtpSyncActivity.h** - Activity class extending `ActivityWithSubactivity` with states: WIFI_SELECTION, SYNCING, SUCCESS, FAILED
- **src/activities/settings/NtpSyncActivity.cpp** - Full implementation:
- Launches `WifiSelectionActivity` with auto-connect enabled
- On WiFi connect: blocking `waitForNtpSync(8000ms)`
- Shows synced time on success; auto-dismisses after 5 seconds
- Failed state requires manual Back press
- Clean WiFi teardown in `onExit()`
### Modified Files
- **src/activities/settings/SettingsActivity.h** - Added `SyncClock` to `SettingAction` enum
- **src/activities/settings/SettingsActivity.cpp** - Added include, switch case handler, and action in `rebuildClockActions()`
- **lib/I18n/translations/*.yaml** (all 9 files) - Added `STR_SYNC_CLOCK` and `STR_TIME_SYNCED` string keys
- **lib/I18n/I18nKeys.h, I18nStrings.h, I18nStrings.cpp** - Regenerated from YAML
## Build Result
SUCCESS - RAM: 32.7%, Flash: 70.7%
## Follow-up Items
- Non-English translations use English fallback values; translators can update later

View File

@@ -0,0 +1,52 @@
# Manage Books Feature - Implementation
## Task Description
Implemented the full Manage Books feature plan across 10 commits on the `mod/manage-books` branch. The feature adds Archive, Delete, Delete Cache, and Reindex book management actions, an interactive End of Book menu, and a bugfix for Clear Reading Cache.
## Changes Made (10 commits)
### COMMIT 1: `feat(hal): expose rename() on HalStorage`
- `lib/hal/HalStorage.h` / `.cpp` — Added `rename(path, newPath)` forwarding to SDCardManager for file/directory move operations.
### COMMIT 2: `feat(i18n): add string keys for book management feature`
- `lib/I18n/translations/english.yaml` — Added 15 new string keys (STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK, etc.)
- `lib/I18n/I18nKeys.h` — Regenerated via gen_i18n.py
### COMMIT 3: `feat: add BookManager utility and RecentBooksStore::clear()`
- **New:** `src/util/BookManager.h` / `.cpp` — Static utility namespace with `archiveBook`, `unarchiveBook`, `deleteBook`, `deleteBookCache`, `reindexBook`, `isArchived`, `getBookCachePath`. Archive mirrors directory structure under `/.archive/` and renames cache dirs to match new path hashes.
- `src/RecentBooksStore.h` / `.cpp` — Added `clear()` method.
### COMMIT 4: `feat: add BookManageMenuActivity popup sub-activity`
- **New:** `src/activities/home/BookManageMenuActivity.h` / `.cpp` — Contextual popup menu with Archive/Unarchive, Delete, Delete Cache Only, Reindex Book. Supports long-press on Reindex for REINDEX_FULL (includes cover/thumb regeneration).
### COMMIT 5: `refactor: change browse activities to ActivityWithSubactivity`
- `HomeActivity`, `MyLibraryActivity`, `RecentBooksActivity` — Changed base class from `Activity` to `ActivityWithSubactivity`. Added `subActivity` guard at top of `loop()`.
### COMMIT 6: `feat: add long-press Confirm for book management in file browser and recents`
- `MyLibraryActivity` / `RecentBooksActivity` — Long-press Confirm on a book opens BookManageMenuActivity. Actions executed via BookManager, file list refreshed afterward.
### COMMIT 7: `feat: add long-press on HomeActivity for book management and archive browsing`
- `HomeActivity` — Long-press Confirm on recent book opens manage menu. Long-press on Browse Files navigates to `/.archive/`.
- `main.cpp` — Wired `onMyLibraryOpenWithPath` callback through to HomeActivity constructor.
### COMMIT 8: `feat: replace Delete Book Cache with Manage Book in reader menu`
- `EpubReaderMenuActivity` — Replaced DELETE_CACHE menu item with MANAGE_BOOK. Opens BookManageMenuActivity as sub-activity. Added ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK, REINDEX_BOOK_FULL action types.
- `EpubReaderActivity` — Handles new actions via BookManager.
### COMMIT 9: `feat: add EndOfBookMenuActivity replacing static end-of-book text`
- **New:** `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp` — Interactive popup with Archive, Delete, Back to Beginning, Close Book, Close Menu options.
- `EpubReaderActivity` / `XtcReaderActivity` — Replaced static "End of Book" text with EndOfBookMenuActivity.
- `TxtReaderActivity` — Added end-of-book detection (advance past last page triggers menu).
### COMMIT 10: `fix: ClearCacheActivity now clears txt_* caches and recents list`
- `ClearCacheActivity.cpp` — Added `txt_*` to directory prefix check. Calls `RECENT_BOOKS.clear()` after cache wipe.
## New Files
- `src/util/BookManager.h` / `.cpp`
- `src/activities/home/BookManageMenuActivity.h` / `.cpp`
- `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp`
## Follow-up Items
- Test on device: verify all menu interactions, archive/unarchive flow, end-of-book menu behavior
- Verify cache rename works correctly across different book formats
- Consider adding translations for new strings in non-English language files

View File

@@ -0,0 +1,24 @@
# Port Upstream PR #1027: ParsedText Word-Width Cache and Hyphenation Early Exit
## Task
Ported upstream PR #1027 (jpirnay) into the mod. The PR reduces `ParsedText::layoutAndExtractLines` CPU time by 59% via two independent optimizations.
## Changes Made
**`lib/Epub/Epub/ParsedText.cpp`** (single file):
1. Added `#include <cstring>` for `memcpy`/`memcmp`
2. Added 128-entry direct-mapped word-width cache in the anonymous namespace (`WordWidthCacheEntry`, FNV-1a hash, `cachedMeasureWordWidth`). 4 KB in BSS, zero heap allocation.
3. Switched `calculateWordWidths` to use `cachedMeasureWordWidth` instead of `measureWordWidth`
4. Added `lineBreakIndices.reserve(totalWordCount / 8 + 1)` in `computeLineBreaks`
5. In `hyphenateWordAtIndex`: added reusable `prefix` string buffer (avoids per-iteration `substr` allocations) and early exit `break` when prefix exceeds available width (ascending byte-offset order means all subsequent candidates will also be too wide)
**`mod/prs/MERGED.md`**: Added PR #1027 entry (TOC link + full section with context, changes, differences, discussion).
## Key Adaptation
The upstream PR targets `std::list`-based code, but our mod already uses `std::vector` (from PR #1038). List-specific optimizations (splice in `extractLine`, `std::next` vs `std::advance`, `continuesVec` pointer sync) were not applicable. Only the algorithmic improvements were ported.
## Follow-up Items
- None. Port is complete.

View File

@@ -0,0 +1,24 @@
# Port PR #1068: Correct hyphenation of URLs
## Task
Port upstream PR [#1068](https://github.com/crosspoint-reader/crosspoint-reader/pull/1068) by Uri-Tauber into the mod fork. The PR was not yet merged upstream, so it was manually patched in.
## Changes made
1. **`lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`**: Added `case '/':` to `isExplicitHyphen` switch, treating `/` as an explicit hyphen delimiter for URL path segments.
2. **`lib/Epub/Epub/hyphenation/Hyphenator.cpp`**: Replaced the single combined filter in `buildExplicitBreakInfos` with a two-stage check:
- First checks `isExplicitHyphen(cp)`
- Then skips repeated separators (e.g., `//` in `http://`, `--`)
- Then applies strict alphabetic-surround rule only for non-URL separators (`cp != '/' && cp != '-'`)
3. **`mod/prs/MERGED.md`**: Added PR #1068 entry with full documentation.
## Mod enhancements over upstream PR
- Included coderabbit's nitpick suggestion (not yet addressed in upstream) to prevent breaks between consecutive identical separators like `//` and `--`.
## Follow-up items
- None. Port is complete.

View File

@@ -0,0 +1,26 @@
# Implement BmpViewer Activity (upstream PR #887)
## Task
Port the BmpViewer feature from upstream crosspoint-reader PR #887 to the mod/master fork, enabling .bmp file viewing from the file browser.
## Changes Made
### New Files
- **src/activities/util/BmpViewerActivity.h** -- Activity subclass declaration with file path, onGoBack callback, and loadFailed state
- **src/activities/util/BmpViewerActivity.cpp** -- BMP loading/rendering implementation: shows loading indicator, opens file via Storage HAL, parses BMP headers, computes centered position with aspect-ratio-preserving layout, renders via `renderer.drawBitmap()`, draws localized back button hint, handles back button input in loop
### Modified Files
- **src/activities/reader/ReaderActivity.h** -- Added `isBmpFile()` and `onGoToBmpViewer()` private declarations
- **src/activities/reader/ReaderActivity.cpp** -- Added BmpViewerActivity include, `isBmpFile()` implementation, `onGoToBmpViewer()` implementation (sets currentBookPath, exits, enters BmpViewerActivity with goToLibrary callback), BMP routing in `onEnter()` before XTC/TXT checks
- **src/activities/home/MyLibraryActivity.cpp** -- Added `.bmp` to the file extension filter in `loadFiles()`
- **src/components/themes/lyra/LyraTheme.cpp** -- Changed `fillRect` to `fillRoundedRect` in `drawButtonHints()` for both FULL and SMALL button sizes to prevent white rectangles overflowing rounded button borders
## Build Result
- PlatformIO build SUCCESS (default env)
- RAM: 32.7% (107,308 / 327,680 bytes)
- Flash: 71.1% (4,657,244 / 6,553,600 bytes)
## Follow-up Items
- Test on hardware with various BMP files (different sizes, bit depths)
- Consider adding image scaling for oversized BMPs (currently `drawBitmap` handles scaling)
- Future enhancements mentioned in upstream PR: next/prev image navigation, "display and sleep" button

View File

@@ -0,0 +1,27 @@
# Port PR #1055: Byte-level framebuffer writes
**Date:** 2026-02-21
**Task:** Port upstream PR #1055 (jpirnay) into the mod, including coderabbit review nitpick.
## Changes made
### `lib/GfxRenderer/GfxRenderer.h`
- Added two private method declarations: `fillPhysicalHSpanByte` (patterned byte-level span writer) and `fillPhysicalHSpan` (solid-fill wrapper).
### `lib/GfxRenderer/GfxRenderer.cpp`
- Added `#include <cstring>` for `memset`.
- Implemented `fillPhysicalHSpanByte`: bounds-clamped byte-level span writer with MSB-first bit packing, partial-byte masking at edges, and `memset` for aligned middle.
- Implemented `fillPhysicalHSpan`: thin wrapper mapping `state` bool to `0x00`/`0xFF` pattern byte.
- **`drawLine`**: Replaced per-pixel loops for axis-aligned lines with orientation-aware `fillPhysicalHSpan` calls (vertical lines in Portrait, horizontal lines in Landscape).
- **`fillRect`**: Replaced per-row `drawLine` loop with orientation-specific byte-level fast paths for all 4 orientations.
- **`fillRectDither`**: Replaced per-pixel `drawPixelDither` loops for DarkGray/LightGray with orientation-aware `fillPhysicalHSpanByte` using pre-computed byte patterns.
- **`fillPolygon`** (coderabbit nitpick): Added `fillPhysicalHSpan` fast path for Landscape orientations in the scanline inner loop.
### `mod/prs/MERGED.md`
- Appended PR #1055 entry with full documentation.
## Differences from upstream
- `fillPolygon` landscape optimization (from coderabbit review nitpick) was applied as a mod enhancement.
## Follow-up items
- None. Build verified successful.

View File

@@ -0,0 +1,28 @@
# README.md Update for Mod Branch
**Date:** 2026-02-21
## Task
Updated `README.md` to distinguish this mod fork from upstream CrossPoint Reader, documenting all mod-exclusive features, updated feature status, upstream PR ports, and upstream compatibility notes.
## Changes Made
**File:** `README.md`
1. **Header/intro** — Renamed to "CrossPoint Reader (Mod)" with a link to the upstream repo and a summary of what the mod provides. Added blockquote linking to upstream.
2. **Motivation** — Added a paragraph explaining the mod's purpose (faster iteration on features and fixes). Fixed "truely" typo.
3. **Features & Usage checklist** — Updated to reflect current mod capabilities:
- Checked: image support (JPEG/PNG), table rendering, bookmarks, dictionary, book management, clock, letterbox fill, placeholder covers, screen rotation (4 orientations), end-of-book menu, silent pre-indexing
- Added sub-items: file extensions, expandable rows
- Still unchecked: user provided fonts, full UTF, EPUB picker with cover art
4. **New section: "What This Mod Adds"** — Organized by category (Reading Enhancements, Home Screen & Navigation, Book Management, Reader Menu, Display & Rendering, Performance). Documents bookmarks, dictionary, tables, end-of-book menu, clock/NTP, adaptive home screen, file browser improvements, archive system, manage book menu, long-press actions, letterbox fill, landscape CCW, silent pre-indexing, placeholder covers, and 5 ported upstream performance PRs.
5. **New section: "Upstream Compatibility"** — Documents what's missing from the mod (BmpViewer, Catalan translations), build differences (font/hyphenation omissions, version string), and links to `mod/prs/MERGED.md`.
6. **Installing** — Replaced web flasher instructions with `pio run -e mod --target upload`. Noted `env:default` alternative.
7. **Development** — Updated clone command to use `-b mod/master`. Added build environments table (`mod` vs `default`). Updated flash command.
8. **Contributing** — Simplified to note this is a personal mod fork with a link to the upstream repo for contributions.
## Follow-up Items
- `USER_GUIDE.md` is out of date and does not document mod features (bookmarks, dictionary, clock, book management, etc.)
- The data caching section in Internals could be expanded to mention mod-specific cache files (`bookmarks.bin`, `book_settings.bin`, `dictionary.cache`, image `.pxc` files)

View File

@@ -0,0 +1,28 @@
# Boot NTP Auto-Sync Feature
## Task
Add a "Auto Sync on Boot" toggle in Clock Settings that silently syncs time via WiFi/NTP during boot, with no user interaction required and graceful failure handling.
## Changes Made
### New files
- `src/util/BootNtpSync.h` / `src/util/BootNtpSync.cpp` -- Background FreeRTOS task that scans WiFi, connects to a saved network, runs NTP sync, then tears down WiFi. Provides `start()`, `cancel()`, and `isRunning()` API.
### Modified files
- `src/CrossPointSettings.h` -- Added `uint8_t autoNtpSync = 0` field
- `src/CrossPointSettings.cpp` -- Added persistence (write/read) for the new field
- `src/SettingsList.h` -- Added Toggle entry under Clock category
- `lib/I18n/translations/*.yaml` (all 9 languages) -- Added `STR_AUTO_NTP_SYNC` string
- `lib/I18n/I18nKeys.h` / `lib/I18n/I18nStrings.cpp` -- Regenerated via `gen_i18n.py`
- `src/main.cpp` -- Calls `BootNtpSync::start()` during setup (non-blocking)
- `src/activities/settings/NtpSyncActivity.cpp` -- Added `BootNtpSync::cancel()` guard
- `src/activities/network/WifiSelectionActivity.cpp` -- Added cancel guard
- `src/activities/settings/OtaUpdateActivity.cpp` -- Added cancel guard
- `src/activities/settings/KOReaderAuthActivity.cpp` -- Added cancel guard
- `src/activities/reader/KOReaderSyncActivity.cpp` -- Added cancel guard
- `src/activities/network/CrossPointWebServerActivity.cpp` -- Added cancel guard
## Follow-up Items
- Translations: all non-English languages currently use English fallback for the new string
- The FreeRTOS task uses 4096 bytes of stack; monitor for stack overflow if WiFi scan behavior changes
- WiFi adds ~50-60KB RAM pressure during the sync window (temporary)

View File

@@ -0,0 +1,43 @@
# Port Upstream PRs #1207 and #1209
## Task
Ported two upstream PRs from crosspoint-reader/crosspoint-reader to the fork:
- **PR #1207**: `fix: use HTTPClient::writeToStream for downloading files from OPDS`
- **PR #1209**: `feat: Support for multiple OPDS servers`
Cherry-picking was not possible due to significant divergences (WiFiClient vs NetworkClient naming, i18n changes, binary vs JSON settings, missing JsonSettingsIO/ObfuscationUtils).
## Changes Made
### PR #1207 (2 files modified)
- `src/network/HttpDownloader.cpp` — Added `FileWriteStream` class, replaced manual chunked download loop with `HTTPClient::writeToStream`, improved Content-Length handling and post-download validation
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — Truncated download status text to fit screen width
### PR #1209 (6 files created, ~15 files modified, 2 files deleted)
**New files:**
- `src/OpdsServerStore.h` / `src/OpdsServerStore.cpp` — Singleton store for up to 8 OPDS servers with MAC-based XOR+base64 password obfuscation and JSON persistence (self-contained — fork lacks JsonSettingsIO/ObfuscationUtils, so persistence was inlined)
- `src/activities/settings/OpdsServerListActivity.h` / `.cpp` — Dual-mode activity: settings list (add/edit/delete) and server picker
- `src/activities/settings/OpdsSettingsActivity.h` / `.cpp` — Individual server editor (name, URL, username, password, delete)
**Modified files:**
- `src/network/HttpDownloader.h` / `.cpp` — Added per-call username/password parameters (default empty), removed global SETTINGS dependency
- `src/activities/browser/OpdsBookBrowserActivity.h` / `.cpp` — Constructor accepts `OpdsServer`, uses server-specific credentials and URL, shows server name in header
- `src/activities/home/HomeActivity.h` / `.cpp``hasOpdsUrl``hasOpdsServers`, uses `OPDS_STORE.hasServers()`
- `src/main.cpp` — Added OPDS_STORE loading on boot, server picker when multiple servers configured
- `src/activities/settings/SettingsActivity.cpp` — Replaced CalibreSettingsActivity with OpdsServerListActivity
- `src/network/CrossPointWebServer.h` / `.cpp` — Added REST endpoints: GET/POST /api/opds, POST /api/opds/delete
- `src/network/html/SettingsPage.html` — Added OPDS server management UI (CSS + JS)
- `src/SettingsList.h` — Removed legacy OPDS entries (opdsServerUrl, opdsUsername, opdsPassword)
- 9 translation YAML files — Added STR_ADD_SERVER, STR_SERVER_NAME, STR_NO_SERVERS, STR_DELETE_SERVER, STR_DELETE_CONFIRM, STR_OPDS_SERVERS
**Deleted files:**
- `src/activities/settings/CalibreSettingsActivity.h` / `.cpp`
## Key Adaptation Decisions
- Kept `WiFiClient`/`WiFiClientSecure` naming (fork hasn't adopted `NetworkClient` rename)
- Inlined JSON persistence and MAC-based obfuscation directly into `OpdsServerStore.cpp` (fork lacks `JsonSettingsIO` and `ObfuscationUtils` libraries)
- Legacy single-server settings auto-migrate to new `opds.json` on first boot
## Build Status
Compilation verified: SUCCESS (RAM: 32.8%, Flash: 71.4%)

View File

@@ -0,0 +1,33 @@
# OPDS Post-Download Prompt Screen
## Task
After an OPDS download completes, instead of immediately returning to the catalog listing, show a prompt screen with two options: "Back to Listing" and "Open Book". A 5-second auto-timer executes the default action. A per-server setting controls which option is default (back to listing by default for backward compatibility).
## Changes Made
### New i18n strings
- `lib/I18n/translations/english.yaml` — Added `STR_DOWNLOAD_COMPLETE`, `STR_OPEN_BOOK`, `STR_BACK_TO_LISTING`, `STR_AFTER_DOWNLOAD`
- Regenerated `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.cpp` via `gen_i18n.py`
### Per-server setting
- `src/OpdsServerStore.h` — Added `afterDownloadAction` field to `OpdsServer` (0 = back to listing, 1 = open book)
- `src/OpdsServerStore.cpp` — Serialized/deserialized `after_download` in JSON
### Browser activity
- `src/activities/browser/OpdsBookBrowserActivity.h` — Added `DOWNLOAD_COMPLETE` state, `onGoToReader` callback, `downloadedFilePath`, `downloadCompleteTime`, `promptSelection` members, `executePromptAction()` method
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — On download success: transition to `DOWNLOAD_COMPLETE` state instead of `BROWSING`. Added loop handling (Left/Right to toggle selection, Confirm to execute, Back to go to listing, 5s auto-timer). Added render for prompt screen with bracketed selection UI. Added `executePromptAction()` helper.
### Callback threading
- `src/main.cpp` — Passed `onGoToReader` callback to `OpdsBookBrowserActivity` constructor in both single-server and multi-server code paths
### Settings UI
- `src/activities/settings/OpdsSettingsActivity.cpp` — Incremented `BASE_ITEMS` from 6 to 7. Added "After Download" field at index 6 (toggles between "Back to Listing" and "Open Book"). Shifted Delete to index 7.
### Countdown refinements (follow-up)
- Added live countdown display using `FAST_REFRESH` -- shows "(5s)", "(4s)", etc., updating each second
- Any button press (Left/Right to change selection, Back, Confirm) cancels the countdown entirely instead of resetting it
- Added `countdownActive` bool and `lastCountdownSecond` int to track state without redundant redraws
## Follow-up Items
- Translations for the 4 new strings in non-English languages
- On-device testing of the full flow (download → prompt → open book / back to listing)

View File

@@ -0,0 +1,18 @@
# OPDS Per-Server Default Save Directory
## Task
Add a configurable default download path per OPDS server. The directory picker now starts at the server's saved path instead of always at root. New servers default to "/".
## Changes Made
### Modified files
- **`src/OpdsServerStore.h`** -- Added `downloadPath` field (default `"/"`) to `OpdsServer` struct.
- **`src/OpdsServerStore.cpp`** -- Serialize/deserialize `download_path` in JSON. Existing servers without the field default to `"/"`.
- **`src/activities/settings/OpdsSettingsActivity.cpp`** -- Added "Download Path" as field index 4 (shifted Delete to 5). Uses `DirectoryPickerActivity` as subactivity for folder selection. Displays current path in row value.
- **`src/activities/util/DirectoryPickerActivity.h`** / **`.cpp`** -- Added `initialPath` constructor parameter (default `"/"`). `onEnter()` validates the path exists on disk and falls back to `"/"` if not.
- **`src/activities/browser/OpdsBookBrowserActivity.cpp`** -- Passes `server.downloadPath` as `initialPath` when launching the directory picker.
- **`lib/I18n/translations/*.yaml`** (all 9 languages) -- Added `STR_DOWNLOAD_PATH` with translations.
- **`lib/I18n/I18nKeys.h`**, **`I18nStrings.h`**, **`I18nStrings.cpp`** -- Regenerated.
## Follow-up Items
- Test on device: verify settings UI shows/saves path, picker opens at saved path, fallback works when directory is removed

View File

@@ -0,0 +1,46 @@
# OPDS Server Reordering
## Task
Add the ability to reorder OPDS servers via a `sortOrder` field, editable on-device and with up/down buttons in the web UI.
## Changes Made
### Data Model (`src/OpdsServerStore.h`)
- Added `int sortOrder = 0` field to `OpdsServer` struct
### Store Logic (`src/OpdsServerStore.cpp`)
- `saveToFile()`: persists `sort_order` to JSON
- `loadFromFile()`: reads `sort_order`, assigns sequential defaults to servers missing it, sorts after load
- `migrateFromSettings()`: assigns `sortOrder = 1` to migrated server
- `addServer()`: auto-assigns `sortOrder = max(existing) + 1` when left at 0
- `updateServer()`: re-sorts after update
- Added `sortServers()` private helper: sorts by sortOrder ascending, ties broken case-insensitively by name (falling back to URL)
- Added `moveServer(index, direction)`: swaps sortOrder with adjacent server, re-sorts, saves
### On-Device UI (`src/activities/settings/OpdsSettingsActivity.cpp`)
- Added "Position" as first menu item (index 0), shifting all others by 1
- Uses `NumericStepperActivity` (new) for position editing: numeric stepper with Up/Down and PageForward/PageBack to increment/decrement
- `saveServer()` now re-locates the server by name+url after sort to keep `serverIndex` valid
### NumericStepperActivity (new: `src/activities/util/NumericStepperActivity.{h,cpp}`)
- Reusable numeric stepper modeled after `SetTimezoneOffsetActivity`
- Displays value with highlight rect and up/down arrow indicators
- Up/PageForward increment, Down/PageBack decrement (clamped to min/max)
- Confirm saves, Back cancels
- Side button hints show +/-
### Web API (`src/network/CrossPointWebServer.{h,cpp}`)
- `GET /api/opds`: now includes `sortOrder` in response
- `POST /api/opds`: preserves `downloadPath` and `sortOrder` on update
- New `POST /api/opds/reorder`: accepts `{index, direction: "up"|"down"}`, calls `moveServer()`
### Web UI (`src/network/html/SettingsPage.html`)
- Added up/down arrow buttons to each OPDS server card (hidden at boundaries)
- Added `reorderOpdsServer()` JS function calling the new API endpoint
### i18n
- Added `STR_POSITION` to all 9 translation YAML files
- Regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
## Follow-up Items
- None identified; build passes cleanly.

View File

@@ -0,0 +1,35 @@
# Port Upstream KOReader Sync PRs
## Task
Port three unmerged upstream PRs into the fork:
- PR #1185: Cache KOReader document hash
- PR #1217: Proper KOReader XPath synchronisation
- PR #1090: Push progress and sleep (with silent failure adaptation)
## Changes Made
### PR #1185 — Cache KOReader Document Hash
- **lib/KOReaderSync/KOReaderDocumentId.h**: Added private static helpers `getCacheFilePath`, `loadCachedHash`, `saveCachedHash`
- **lib/KOReaderSync/KOReaderDocumentId.cpp**: Cache lookup before hash computation, persist after; uses mtime fingerprint + file size for validation
### PR #1217 — Proper KOReader XPath Synchronisation
- **lib/KOReaderSync/ChapterXPathIndexer.h/.cpp**: New files — Expat-based on-demand XHTML parsing for bidirectional XPath/progress mapping
- **lib/KOReaderSync/ProgressMapper.h/.cpp**: XPath-first mapping in both directions, percentage fallback, DocFragment 1-based indexing fix, `std::clamp` sanitization
- **docs/contributing/koreader-sync-xpath-mapping.md**: Design doc
### PR #1090 — Push Progress & Sleep (adapted)
Adapted to fork's `ActivityWithSubactivity` + callback architecture (upstream uses `Activity` + `startActivityForResult`).
- **lib/I18n/translations/english.yaml** + auto-generated **I18nKeys.h**: Added `STR_PUSH_AND_SLEEP`
- **src/activities/reader/EpubReaderMenuActivity.h**: Added `PUSH_AND_SLEEP` to `MenuAction` enum and `buildMenuItems()`
- **src/activities/reader/KOReaderSyncActivity.h/.cpp**: Added `SyncMode::PUSH_ONLY`, `deferFinish()` mechanism, PUSH_ONLY paths in `performSync`/`performUpload`/`loop`
- **src/activities/reader/EpubReaderActivity.h/.cpp**: Added `pendingSleep` flag, `extern void enterDeepSleep()`, `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`
**Silent failure**: Both `onCancel` and `onSyncComplete` callbacks set `pendingSleep = true`, so the device sleeps regardless of sync success/failure. No credentials also triggers sleep directly.
## Build
Compiles cleanly on `default` environment (ESP32-C3). RAM: 32.8%, Flash: 71.7%.
## Follow-up Items
- Changes are unstaged — commit when ready
- Other language YAML files will auto-fallback to English for `STR_PUSH_AND_SLEEP`

View File

@@ -0,0 +1,50 @@
# Fresh Replay: Sync mod/master with upstream/master (continued)
## Task
Continue the "Fresh Replay" synchronization of mod/master with upstream/master (HEAD: 170cc25). This session picked up from Phase 2c (GfxRenderer/theme modifications) and completed through Phase 4 (verification).
## Changes Made
### Phase 2c-e: GfxRenderer, themes, SleepActivity, SettingsActivity, platformio
- Added `drawPixelGray` to GfxRenderer for letterbox fill rendering
- Added `PRERENDER_THUMB_HEIGHTS` to UITheme for placeholder cover generation
- Added `[env:mod]` build environment to platformio.ini
- Implemented sleep screen letterbox fill (solid/dithered) with edge caching in SleepActivity
- Added placeholder cover fallback (PlaceholderCoverGenerator) for XTC/TXT/EPUB sleep screens
- Added Clock settings category to SettingsActivity with timezone, NTP sync, set-time actions
- Replaced CalibreSettingsActivity with OpdsServerListActivity for OPDS server management
- Added DynamicEnum rendering support for settings
- Added long-press book management to RecentBooksActivity
### Phase 3: Re-port unmerged upstream PRs
- **#1055** (byte-level framebuffer writes): fillPhysicalHSpan*, optimized fillRect/drawLine/fillRectDither/fillPolygon
- **#1027** (word-width cache): 128-entry FNV-1a cache, hyphenation early exit (7-9% layout speedup)
- **#1068** (URL hyphenation): Already present in upstream
- **#1019** (file extensions in browser): Already present in upstream
- **#1090/#1185/#1217** (KOReader sync): Binary credential store, document hash caching, ChapterXPathIndexer
- **#1209** (OPDS multi-server): OpdsBookBrowserActivity accepts OpdsServer, directory picker, download-complete prompt
- **#857** (Dictionary): Activities already ported in Phase 1/2
- **#1003** (Placeholder covers): Already integrated in Phase 2
### Fixes
- Added `STR_OFF` i18n string for clock format setting
- Fixed include paths (ActivityResult.h from subdirectories)
- Replaced `Epub::isValidThumbnailBmp` with `Storage.exists()` (method doesn't exist in upstream)
- Replaced `StringUtils::checkFileExtension` with `FsHelpers` equivalents
### Image pipeline decision
- Kept upstream's JPEGDEC implementation — mod's picojpeg was a workaround for the older codebase
- No mod-specific image pipeline changes needed
## Branch Status
- **mod/master-resync**: 6 commits ahead of upstream/master (170cc25)
- **mod/backup-pre-sync-2026-03-07**: Safety snapshot of original mod/master
- 189 files changed, ~114,566 insertions, ~379 deletions vs upstream
## Follow-up Items
- Run full PlatformIO build on hardware to verify compilation
- Run clang-format on all modified files
- Test on device: clock display, sleep screen letterbox fill, dictionary, OPDS browsing
- KOReaderSyncActivity PUSH_ONLY mode (from PR #1090) not yet re-added to activity
- Consider adding `StringUtils.h` if other mod code needs `checkFileExtension`
- Update mod version string

View File

@@ -0,0 +1,42 @@
# KOReaderSyncActivity PUSH_ONLY Mode Re-addition
**Date**: 2026-03-07
**Branch**: `mod/master-resync`
## Task
Re-add the `PUSH_ONLY` sync mode to `KOReaderSyncActivity` that was lost during the upstream resync/ActivityManager migration (originally from PR #1090). This mode allows the reader to silently push local progress to the KOReader sync server and then enter deep sleep — no interactive UI.
## Changes
### `src/activities/reader/KOReaderSyncActivity.h`
- Added `enum class SyncMode { INTERACTIVE, PUSH_ONLY }` to the class
- Added `syncMode` constructor parameter (defaults to `INTERACTIVE`)
- Added `deferFinish(bool success)`, `pendingFinish`, `pendingFinishSuccess` for safe async finish from blocking calls
### `src/activities/reader/KOReaderSyncActivity.cpp`
- Implemented `deferFinish()` — sets a flag that `loop()` picks up to call `setResult()`/`finish()`
- `onEnter()`: In PUSH_ONLY mode, silently finish if no credentials (no error UI)
- `performSync()`: In PUSH_ONLY mode, skip remote fetch entirely and go straight to `performUpload()`
- `performUpload()`: In PUSH_ONLY mode, use `deferFinish()` instead of setting UI state on success/failure
- `loop()`: Check `pendingFinish` first and perform deferred finish if set
### `src/activities/ActivityManager.h`
- Added `requestSleep()` / `isSleepRequested()` — allows activities to signal that the device should enter deep sleep. Checked by the main loop.
### `src/main.cpp`
- Added `activityManager.isSleepRequested()` check in the main loop, before the auto-sleep timeout check
### `src/activities/reader/EpubReaderMenuActivity.h` / `.cpp`
- Added `PUSH_AND_SLEEP` to the `MenuAction` enum
- Added menu item `{PUSH_AND_SLEEP, STR_PUSH_AND_SLEEP}` between SYNC and CLOSE_BOOK
### `src/activities/reader/EpubReaderActivity.cpp`
- Added `#include "activities/ActivityManager.h"`
- Added `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`: launches `KOReaderSyncActivity` in `PUSH_ONLY` mode, then calls `activityManager.requestSleep()` on completion (regardless of success/failure)
### `lib/I18n/translations/english.yaml` / `lib/I18n/I18nKeys.h`
- Added `STR_PUSH_AND_SLEEP: "Push & Sleep"` and regenerated I18n keys
## Follow-up Items
- None

View File

@@ -0,0 +1,61 @@
# Missing Mod Features Audit — Implementation
**Date**: 2026-03-07
**Branch**: `mod/master-resync`
## Task
Comprehensive audit of `mod/master-resync` vs `mod/backup-pre-sync-2026-03-07` identified 4 mod features lost during the upstream resync. All 4 have been re-implemented.
## Changes
### 1. EndOfBookMenuActivity wired into EpubReaderActivity (HIGH)
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
- Added `pendingEndOfBookMenu` and `endOfBookMenuOpened` flags
- In `render()`: when reaching end-of-book, sets `pendingEndOfBookMenu = true` (deferred to avoid render-lock deadlock)
- In `loop()`: checks flag and launches `EndOfBookMenuActivity` via `startActivityForResult`
- Result handler covers all 6 actions: ARCHIVE (→ goHome), DELETE (→ goHome), TABLE_OF_CONTENTS (→ last chapter), BACK_TO_BEGINNING (→ spine 0), CLOSE_BOOK (→ goHome), CLOSE_MENU (→ stay at end)
- Added `#include "EndOfBookMenuActivity.h"` and `#include "util/BookManager.h"`
### 2. Book management from reader menu (MEDIUM)
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
- Added `ARCHIVE_BOOK`, `DELETE_BOOK`, `REINDEX_BOOK` to `MenuAction` enum
- Added corresponding menu items between CLOSE_BOOK and DELETE_CACHE
- Added handlers in `onReaderMenuConfirm`: each calls `BookManager::archiveBook/deleteBook/reindexBook` then `activityManager.goHome()`
### 3. Silent next-chapter pre-indexing (MEDIUM)
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
- Added `preIndexedNextSpine` field and `silentIndexNextChapterIfNeeded()` method
- Triggers when user is on second-to-last or last page of a chapter
- Creates section file for `currentSpineIndex + 1` in advance
- Called after every page turn in `loop()`
- ~35 lines of self-contained implementation
### 4. Letterbox fill toggle in reader menu (LOW)
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
- Added `LETTERBOX_FILL` to `MenuAction` enum
- Added `bookCachePath` constructor parameter (with default `""` for backward compat)
- Added per-book `pendingLetterboxFill`, `letterboxFillLabels`, `letterboxFillToIndex()`, `indexToLetterboxFill()`, `saveLetterboxFill()`
- Cycles Default → Dithered → Solid → None → Default on Confirm
- Renders current value on right edge of menu item
- Loads/saves per-book setting via `BookSettings`
- Updated call site in `EpubReaderActivity` to pass `epub->getCachePath()`
## Audit False Positives (confirmed NOT gaps)
- GfxRenderer kerning/ligatures/wrappedText — present on resync
- HttpDownloader auth fallback — present with OPDS settings fallback
- Lyra3CoversTheme — exists on resync
- ActivityWithSubactivity → Activity migration — intentional upstream change
- EndOfBookMenuActivity callbacks → setResult/finish — correctly migrated
## Follow-up Items
- None

View File

@@ -0,0 +1,23 @@
# Fix mod build environment compilation errors
## Task
Fix compilation errors in the `mod` PlatformIO build environment after the upstream resync. The `default` environment was also verified.
## Changes Made
### Include path fixes (11 files)
- `src/activities/{reader,settings,util}/*.cpp`: Changed bare `#include "ActivityResult.h"` to `#include "activities/ActivityResult.h"` (10 files)
- `src/activities/reader/DictionarySuggestionsActivity.cpp`: Changed `#include "RenderLock.h"` to `#include "activities/RenderLock.h"`
### API compatibility fixes
- `src/util/Dictionary.h`: Replaced invalid `class FsFile;` forward declaration with `#include <HalStorage.h>` (`FsFile` is now a `using` alias)
- `lib/Epub/Epub/blocks/TextBlock.h`: Added `getWordXpos()` public accessor
- `lib/GfxRenderer/GfxRenderer.{h,cpp}`: Re-added `drawTextRotated90CCW()` with `Rotated90CCW` enum value and coordinate mapping, adapted to new fixed-point rendering
- `src/activities/reader/{DictionarySuggestionsActivity,DictionaryWordSelectActivity,LookedUpWordsActivity}.cpp`: Fixed `setResult()` rvalue ref binding (6 lambdas)
- `src/activities/reader/EpubReaderActivity.cpp`: Fixed `std::max(uint8_t, int)` type mismatch
- `src/util/StringUtils.{h,cpp}`: Re-added `checkFileExtension()` and `sortFileList()` functions
- `src/RecentBooksStore.{h,cpp}`: Added missing `removeBook()` method
## Follow-up
- Both `mod` and `default` environments build successfully at 95.2% flash usage
- No functional testing performed yet (on-device verification needed)

View File

@@ -0,0 +1,42 @@
# Restore Lost Mod Features Post-Upstream-Sync
## Task
Implemented the full plan to restore 8 mod-specific features lost during the upstream sync, organized into 5 phased commits.
## Changes Made
### Phase 0 — Commit Pending Changes (`4627ec9`)
- Staged and committed 22 modified files from previous sessions (ADC fix, logging guard, include path fixes, etc.) as a clean baseline.
### Phase 1 — UI Rendering Fixes (`f44657a`)
- **Clock display**: Added clock rendering to `BaseTheme::drawHeader()` and `LyraTheme::drawHeader()` supporting OFF/AM-PM/24H formats and Small/Medium/Large sizes.
- **Placeholder covers**: Fixed `splitWords()` in `PlaceholderCoverGenerator.cpp` to treat `\n`, `\r`, `\t` as whitespace (not just spaces). Removed `drawBorder()` call since the UI already draws frames around book cards.
### Phase 2 — Reader Menu Restructure (`0493f30`)
- **Manage Book submenu**: Replaced 4 individual top-level menu items (Archive, Delete, Reindex, Delete Cache) with single "Manage Book" entry that launches `BookManageMenuActivity`.
- **Dictionary submenu**: Created new `DictionaryMenuActivity` and replaced 3 scattered dictionary items with single "Dictionary" entry.
- **Long-press TOC**: Added 700ms long-press detection on Confirm button to open Table of Contents directly, bypassing the menu.
- Added `STR_DICTIONARY` i18n key and regenerated I18nKeys.h/I18nStrings.h.
### Phase 3 — Settings and Indexing (`22c1892`)
- **Letterbox Fill position**: Moved from bottom of settings list to immediately after Sleep Screen Cover Filter.
- **Indexing Display setting**: Added to Display section with Popup/Status Bar Text/Status Bar Icon modes.
- **Silent indexing**: Restored proactive next-chapter indexing logic and status bar indicator (text or hourglass icon) in `EpubReaderActivity`.
### Phase 4 — Book Preparation (`a5ca15d`)
- **Prerender on first open**: Restored the cover/thumbnail prerender block in `EpubReaderActivity::onEnter()` with "Preparing book..." popup and progress bar.
- Added `isValidThumbnailBmp()`, `generateInvalidFormatCoverBmp()`, and `generateInvalidFormatThumbBmp()` methods to `Epub` class.
## Files Modified
- `src/components/themes/BaseTheme.cpp`, `src/components/themes/lyra/LyraTheme.cpp` — clock display
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — whitespace splitting, border removal
- `src/activities/reader/EpubReaderMenuActivity.h/.cpp` — menu restructure
- `src/activities/reader/DictionaryMenuActivity.h/.cpp` — new submenu (created)
- `src/activities/reader/EpubReaderActivity.h/.cpp` — long-press TOC, MANAGE_BOOK/DICTIONARY handlers, silent indexing, book prerender
- `src/SettingsList.h` — settings reordering, indexing display entry
- `lib/Epub/Epub.h`, `lib/Epub/Epub.cpp` — BMP validation and fallback generation methods
- `lib/I18n/translations/english.yaml`, `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h` — STR_DICTIONARY key
## Follow-up Items
- On-device verification of all 8 features by user
- Optional: verify `pio run -e default` still builds cleanly

View File

@@ -0,0 +1,38 @@
# Fix Reader Bugs: Covers, Indexing, TOC, Cache Deletion
**Date:** 2026-03-08
**Commit:** `022f519` on `mod/master-resync`
## Task Description
Fixed four bugs reported after the upstream sync feature restoration:
1. **Placeholder cover text rendering** — only first letter of each word visible
2. **Silent indexing indicator** — wrong timing (shown on first page, not during actual indexing) and icon positioned above the status bar
3. **Long-press confirm for TOC** — immediately selected the first chapter upon button release
4. **Book cache deletion from home screen** — showed "no open books" and required double confirm press
## Changes Made
### lib/PlaceholderCover/PlaceholderCoverGenerator.cpp
- Added `#include <EpdFontData.h>` for `fp4::toPixel()`
- Fixed `renderGlyph()`: `glyph->advanceX` is 12.4 fixed-point, not pixels — was advancing cursor ~16x too far, causing only the first character of each word to be visible
- Fixed `getCharAdvance()`: same fixed-point conversion needed for space width calculation in word wrapping
### src/activities/reader/EpubReaderActivity.cpp
- Removed `silentIndexNextChapterIfNeeded()` call from `loop()` (line 385) — this blocked the UI before render, preventing the indicator from ever showing. The backup branch only called it from `render()`, after the page was drawn.
- Fixed indexing icon Y position: changed `textY - kIndexingIconSize + 2` to `textY + 2` to align within the status bar alongside battery/text
- Passed `consumeFirstRelease` flag when creating chapter selection activity from long-press path
### src/activities/reader/EpubReaderChapterSelectionActivity.h/.cpp
- Added `ignoreNextConfirmRelease` member and `consumeFirstRelease` constructor parameter
- In `loop()`, consumes the first Confirm release when opened via long-press, preventing immediate selection of the first TOC item
### src/activities/home/HomeActivity.cpp
- In `openManageMenu()` callback: reset `ignoreNextConfirmRelease = false` to fix double-press bug
- Replaced `recentBooks.clear()` with `loadRecentBooks()` to reload remaining books from persistent store after deletion
## Follow-up Items
- Test all four fixes on device to verify correct behavior
- Verify placeholder covers render full title/author text at both thumbnail and full-cover sizes

View File

@@ -0,0 +1,62 @@
# Port 5 Upstream PRs (#1329, #1143, #1172, #1320, #1325)
**Date:** 2026-03-08
**Task:** Port unmerged upstream PRs to the mod across 4 feature branches
## Summary
Ported 5 upstream PRs from `crosspoint-reader/crosspoint-reader` to the mod codebase, organized across 4 phased feature branches:
### Phase 1: PR #1329 — ReaderUtils refactor
**Branch:** `port/1329-reader-utils`
- Created `src/activities/reader/ReaderUtils.h` with shared reader utilities: `GO_HOME_MS`, `applyOrientation()`, `detectPageTurn()`, `displayWithRefreshCycle()`, `renderAntiAliased()`
- Refactored `EpubReaderActivity.cpp` and `TxtReaderActivity.cpp` to use ReaderUtils
- Applied CodeRabbit's `storeBwBuffer()` null-check suggestion
### Phase 2: PRs #1143 + #1172 — TOC fragment navigation + multi-spine TOC
**Branch:** `port/1143-1172-toc-navigation`
- Extended `Section.h/.cpp` with TOC boundary tracking (`tocBoundaries`, `buildTocBoundaries()`, page lookup methods)
- Added TOC anchor page breaks to `ChapterHtmlSlimParser` (chapters start on fresh pages)
- Added TOC-aware navigation to `EpubReaderActivity` (long-press walks TOC entries, status bar shows subchapter title)
- Updated `EpubReaderChapterSelectionActivity` to pass and accept `tocIndex`
- Added multi-spine chapter caching (`cacheMultiSpineChapter()`)
- Incremented `SECTION_FILE_VERSION` from 18 to 19
- Preserved mod's footnote support, image rendering options, and Activity base class
### Phase 3: PR #1320 — JPEG resource cleanup
**Branch:** `port/1320-jpeg-cleanup`
- Added `ScopedCleanup` RAII struct to `JpegToBmpConverter.cpp`
- Removed scattered `free()`/`delete` calls
- Changed `rowCount` from `uint16_t*` to `uint32_t*` to prevent overflow
### Phase 4: PR #1325 — Settings tab label
**Branch:** `port/1325-settings-label`
- Dynamic confirm label in `SettingsActivity.cpp` shows next category name when tab bar is focused
## Files changed
| File | Phase | Change type |
|------|-------|-------------|
| `src/activities/reader/ReaderUtils.h` | 1 | New file |
| `src/activities/reader/EpubReaderActivity.cpp` | 1, 2 | Refactored |
| `src/activities/reader/EpubReaderActivity.h` | 2 | Extended |
| `src/activities/reader/TxtReaderActivity.cpp` | 1 | Refactored |
| `lib/Epub/Epub/Section.h` | 2 | Extended |
| `lib/Epub/Epub/Section.cpp` | 2 | Extended |
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` | 2 | Extended |
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` | 2 | Extended |
| `src/activities/ActivityResult.h` | 2 | Extended |
| `src/activities/reader/EpubReaderChapterSelectionActivity.h` | 2 | Extended |
| `src/activities/reader/EpubReaderChapterSelectionActivity.cpp` | 2 | Extended |
| `lib/JpegToBmpConverter/JpegToBmpConverter.cpp` | 3 | Refactored |
| `src/activities/settings/SettingsActivity.cpp` | 4 | Modified |
| `mod/prs/MERGED.md` | Housekeeping | Updated |
| `mod/docs/upstream-sync.md` | Housekeeping | Updated |
## Follow-up items
- All 4 branches need hardware testing before merging to `mod/master`
- Test TOC navigation with multi-chapter spine EPUBs (short story collections, academic texts)
- Test JPEG resource cleanup with large image-heavy EPUBs
- Verify `SECTION_FILE_VERSION` bump invalidates old caches properly (delete `.crosspoint/` on SD card)
- When upstream merges these PRs, these ports should be dropped during the next sync

View File

@@ -0,0 +1,31 @@
# Fix Three Reader Bugs on mod/master
**Date**: 2026-03-08
**Branch**: mod/master
**Commit**: 422cad7
## Task
Fixed three bugs reported after merging the upstream PR ports:
## Changes
### Bug 1: Confirm button ignored after TOC navigation
- **File**: `src/activities/reader/EpubReaderActivity.cpp`
- **Root cause**: `ignoreNextConfirmRelease` was set to `true` on long-press but never cleared after being transferred to the child `EpubReaderChapterSelectionActivity`. The parent's flag remained `true`, causing the next Confirm press to be silently consumed.
- **Fix**: Added `ignoreNextConfirmRelease = false;` immediately after capturing the value into `consumeRelease`.
### Bug 2: Footnotes inaccessible from reader menu
- **File**: `src/activities/reader/EpubReaderMenuActivity.cpp`
- **Root cause**: `buildMenuItems()` accepted a `hasFootnotes` parameter but never used it to add the `MenuAction::FOOTNOTES` entry.
- **Fix**: Added conditional `FOOTNOTES` menu item after DICTIONARY when `hasFootnotes` is true.
### Bug 3: Phantom screen re-render at idle
- **File**: `src/main.cpp`
- **Root cause**: The clock update logic in `loop()` called `activityManager.requestUpdate()` every minute when the displayed minute changed. Reader activities don't show the clock, but still received full page re-renders, causing visible e-ink flashes while idle.
- **Fix**: Guarded the clock block with `!activityManager.isReaderActivity()`.
## Follow-up
- Hardware testing needed for all three fixes
- Bug 3 fix means the clock won't update while reading; it resumes updating when returning to non-reader activities

View File

@@ -0,0 +1,28 @@
# Restore Preferred Orientation Settings and Long-Press Sub-Menu
**Date**: 2026-03-08
**Branch**: mod/master
**Commit**: 0d8a3fd
## Task
Restore two orientation preference features lost during the upstream PR resync:
1. Settings UI entries for preferred portrait/landscape modes
2. Long-press sub-menu on the reader menu's orientation toggle
## Changes
### Settings UI and Persistence
- **`src/SettingsList.h`**: Added `DynamicEnum` entries for `preferredPortrait` (Portrait/Inverted) and `preferredLandscape` (Landscape CW/Landscape CCW) in the Reader settings category. Uses getter/setter lambdas to map between sequential indices and non-sequential orientation enum values.
- **`src/JsonSettingsIO.cpp`**: Added manual JSON save/load for both fields with validation (rejects invalid orientation values, falls back to defaults).
### Long-Press Orientation Sub-Menu
- **`src/activities/reader/EpubReaderMenuActivity.h`**: Added `orientationSubMenuOpen`, `orientationSubMenuIndex`, and `ignoreNextConfirmRelease` state flags.
- **`src/activities/reader/EpubReaderMenuActivity.cpp`**:
- `loop()`: Long-press (700ms) on Confirm when the orientation item is selected opens the sub-menu. Sub-menu handles its own Up/Down/Confirm/Back input. Added `ignoreNextConfirmRelease` guard to prevent the long-press release from immediately selecting.
- `render()`: When sub-menu is open, renders a centered list of all 4 orientations with the current one marked with `*`. Uses the same gutter/hint layout as the main menu.
## Follow-up
- Hardware testing needed for both features
- Translations for `STR_PREFERRED_PORTRAIT` and `STR_PREFERRED_LANDSCAPE` only exist in English; other languages fall back automatically

View File

@@ -0,0 +1,43 @@
# Port Upstream PRs #1311, #1322 + Verify #1329
**Date:** 2026-03-08
**Task:** Port two upstream PRs and verify alignment of a previously-ported PR that was recently merged.
## Changes Made
### PR #1311 -- Fix inter-word spacing rounding error (UNMERGED, ported as mod feature)
Replaced `getSpaceKernAdjust()` with `getSpaceAdvance()` which combines space glyph advance and flanking kern values into a single fixed-point sum before pixel snapping, fixing +/-1 px rounding drift in inter-word spacing.
**Files modified:**
- `lib/GfxRenderer/GfxRenderer.h` -- replaced declaration
- `lib/GfxRenderer/GfxRenderer.cpp` -- replaced implementation (single-snap pattern)
- `lib/Epub/Epub/ParsedText.h` -- removed `spaceWidth` parameter from 3 internal functions
- `lib/Epub/Epub/ParsedText.cpp` -- updated all 4 call sites to use `getSpaceAdvance()`
### PR #1322 -- Early exit on fillUncompressedSizes (MERGED, ported for immediate use)
Added `targetCount` variable and early `break` when all ZIP central-directory targets are matched.
**Files modified:**
- `lib/ZipFile/ZipFile.cpp` -- 5-line addition
### PR #1329 -- Reader utils refactor (MERGED, verification only)
Confirmed our existing port matches the upstream merged version (commit `cd508d2`) line-for-line. No code changes needed.
### Tracking documentation updated
- `mod/docs/upstream-sync.md` -- added #1311, #1322; updated #1329 status to MERGED
- `mod/prs/MERGED.md` -- added detailed entries for #1311 and #1322; updated #1329 author and status
## Build Result
SUCCESS -- zero compiler errors/warnings from our changes. Only pre-existing i18n translation warnings.
## Follow-up Items
- #1311: Will be dropped during next sync if/when merged upstream
- #1322: Will be dropped during next sync (already merged upstream)
- #1329: Will be dropped during next sync (already merged upstream)
- Hardware testing recommended: verify text layout rendering after spacing fix (#1311)

View File

@@ -0,0 +1,99 @@
# KOReader Sync XPath Mapping
This note documents how CrossPoint maps reading positions to and from KOReader sync payloads.
## Problem
CrossPoint internally stores position as:
- `spineIndex` (chapter index, 0-based)
- `pageNumber` + `totalPages`
KOReader sync payload stores:
- `progress` (XPath-like location)
- `percentage` (overall progress)
A direct 1:1 mapping is not guaranteed because page layout differs between engines/devices.
## DocFragment Index Convention
KOReader uses **1-based** XPath predicates throughout, following standard XPath conventions.
The first EPUB spine item is `DocFragment[1]`, the second is `DocFragment[2]`, and so on.
CrossPoint stores spine items as 0-based indices internally. The conversion is:
- **Generating XPath (to KOReader):** `DocFragment[spineIndex + 1]`
- **Parsing XPath (from KOReader):** `spineIndex = DocFragment[N] - 1`
Reference: [koreader/koreader#11585](https://github.com/koreader/koreader/issues/11585) confirms this
via a KOReader contributor mapping spine items to DocFragment numbers.
## Current Strategy
### CrossPoint -> KOReader
Implemented in `ProgressMapper::toKOReader`.
1. Compute overall `percentage` from chapter/page.
2. Attempt to compute a real element-level XPath via `ChapterXPathIndexer::findXPathForProgress`.
3. If XPath extraction fails, fallback to synthetic chapter path:
- `/body/DocFragment[spineIndex + 1]/body`
### KOReader -> CrossPoint
Implemented in `ProgressMapper::toCrossPoint`.
1. Attempt to parse `DocFragment[N]` from incoming XPath; convert N to 0-based `spineIndex = N - 1`.
2. If valid, attempt XPath-to-offset mapping via `ChapterXPathIndexer::findProgressForXPath`.
3. Convert resolved intra-spine progress to page estimate.
4. If XPath path is invalid/unresolvable, fallback to percentage-based chapter/page estimation.
## ChapterXPathIndexer Design
The module reparses **one spine XHTML** on demand using Expat and builds temporary anchors:
Source-of-truth note: XPath anchors are built from the original EPUB spine XHTML bytes (zip item contents), not from CrossPoint's distilled section render cache. This is intentional to preserve KOReader XPath compatibility.
- anchor: `<xpath, textOffset>`
- `textOffset` counts non-whitespace bytes
- When multiple anchors exist for the same path, the one with the **smallest** textOffset is used
(start of element), not the latest periodic anchor.
Forward lookup (CrossPoint → XPath): uses `upper_bound` to find the last anchor at or before the
target text offset, ensuring the returned XPath corresponds to the element the user is currently
inside rather than the next element.
Matching for reverse lookup:
1. exact path match — reported as `exact=yes`
2. index-insensitive path match (`div[2]` vs `div[3]` tolerated) — reported as `exact=no`
3. ancestor fallback — reported as `exact=no`
If no match is found, caller must fallback to percentage.
## Memory / Safety Constraints (ESP32-C3)
The implementation intentionally avoids full DOM storage.
- Parse one chapter only.
- Keep anchors in transient vectors only for duration of call.
- Free XML parser and chapter byte buffer on all success/failure paths.
- No persistent cache structures are introduced by this module.
## Known Limitations
- Page number on reverse mapping is still an estimate (renderer differences).
- XPath mapping intentionally uses original spine XHTML while pagination comes from distilled renderer output, so minor roundtrip page drift is expected.
- Image-only/low-text chapters may yield coarse anchors.
- Extremely malformed XHTML can force fallback behavior.
## Operational Logging
`ProgressMapper` logs mapping source in reverse direction:
- `xpath` when XPath mapping path was used
- `percentage` when fallback path was used
It also logs exactness (`exact=yes/no`) for XPath matches. Note that `exact=yes` is only set for
a full path match with correct indices; index-insensitive and ancestor matches always log `exact=no`.

View File

@@ -15,6 +15,7 @@ This guide explains the multi-language support system in CrossPoint Reader.
- Ukrainian
- Polish
- Danish
- Turkish
---

View File

@@ -1,6 +1,6 @@
# Translators
Below is a list of users and languages CrossPoint may support in the future.
Below is a list of users and languages CrossPoint may support in the future.
Note because a language is below does not mean there is official support for the language at this time.
## Contributing
@@ -35,9 +35,11 @@ If you'd like to add your name to this list, please open a PR adding yourself an
- [yeyeto2788](https://github.com/yeyeto2788)
- [Skrzakk](https://github.com/Skrzakk)
- [pablohc](https://github.com/pablohc)
- [DaniPhii](https://github.com/DaniPhii)
## Swedish
- [dawiik](https://github.com/dawiik)
- [steka](https://github.com/steka)
## Romanian
- [ariel-lindemann](https://github.com/ariel-lindemann)

View File

@@ -1,6 +1,7 @@
#include "Epub.h"
#include <FsHelpers.h>
#include <HalDisplay.h>
#include <HalStorage.h>
#include <JpegToBmpConverter.h>
#include <Logging.h>
@@ -103,14 +104,11 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
pos += strlen(pattern);
const auto endPos = coverPageHtml.find('"', pos);
if (endPos != std::string::npos) {
const auto ref = coverPageHtml.substr(pos, endPos - pos);
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
// Check if it's an image file
if (ref.length() >= 4) {
const auto ext = ref.substr(ref.length() - 4);
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") {
imageRef = ref;
break;
}
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) {
imageRef = ref;
break;
}
}
pos = coverPageHtml.find(pattern, pos);
@@ -541,8 +539,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
return false;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
if (FsHelpers::hasJpgExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
@@ -575,7 +572,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
return success;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
if (FsHelpers::hasPngExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverPngTempPath = getCachePath() + "/.cover.png";
@@ -629,8 +626,7 @@ bool Epub::generateThumbBmp(int height) const {
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
LOG_DBG("EBP", "No known cover image for thumbnail");
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
} else if (FsHelpers::hasJpgExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
@@ -666,7 +662,7 @@ bool Epub::generateThumbBmp(int height) const {
}
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
return success;
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
} else if (FsHelpers::hasPngExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
const auto coverPngTempPath = getCachePath() + "/.cover.png";
@@ -711,6 +707,171 @@ bool Epub::generateThumbBmp(int height) const {
return false;
}
bool Epub::isValidThumbnailBmp(const std::string& bmpPath) {
if (!Storage.exists(bmpPath.c_str())) {
return false;
}
FsFile file = Storage.open(bmpPath.c_str());
if (!file) {
LOG_ERR("EBP", "Failed to open thumbnail BMP at path: %s", bmpPath.c_str());
return false;
}
size_t fileSize = file.size();
if (fileSize == 0) {
LOG_DBG("EBP", "Thumbnail BMP is empty (no cover marker) at path: %s", bmpPath.c_str());
file.close();
return false;
}
uint8_t header[2];
size_t bytesRead = file.read(header, 2);
if (bytesRead != 2) {
LOG_ERR("EBP", "Failed to read thumbnail BMP header at path: %s", bmpPath.c_str());
file.close();
return false;
}
file.close();
return header[0] == 'B' && header[1] == 'M';
}
bool Epub::generateInvalidFormatThumbBmp(int height) const {
const int width = height * 0.6;
const int rowBytes = ((width + 31) / 32) * 4;
const int imageSize = rowBytes * height;
const int fileSize = 14 + 40 + 8 + imageSize;
const int dataOffset = 14 + 40 + 8;
FsFile thumbBmp;
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
return false;
}
thumbBmp.write('B');
thumbBmp.write('M');
thumbBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
uint32_t reserved = 0;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
uint32_t dibHeaderSize = 40;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
int32_t bmpWidth = width;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
int32_t bmpHeight = -height;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
uint16_t planes = 1;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
uint16_t bitsPerPixel = 1;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
uint32_t compression = 0;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
thumbBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
int32_t ppmX = 2835;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
int32_t ppmY = 2835;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
uint32_t colorsUsed = 2;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
uint32_t colorsImportant = 2;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
thumbBmp.write(black, 4);
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00};
thumbBmp.write(white, 4);
for (int y = 0; y < height; y++) {
std::vector<uint8_t> rowData(rowBytes, 0xFF);
const int scaledY = (y * width) / height;
const int thickness = 2;
for (int x = 0; x < width; x++) {
bool drawPixel = false;
if (std::abs(x - scaledY) <= thickness) drawPixel = true;
if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true;
if (drawPixel) {
const int byteIndex = x / 8;
const int bitIndex = 7 - (x % 8);
rowData[byteIndex] &= static_cast<uint8_t>(~(1 << bitIndex));
}
}
thumbBmp.write(rowData.data(), rowBytes);
}
thumbBmp.close();
LOG_DBG("EBP", "Generated invalid format thumbnail BMP");
return true;
}
bool Epub::generateInvalidFormatCoverBmp(bool cropped) const {
const int hwW = static_cast<int>(HalDisplay::DISPLAY_WIDTH);
const int hwH = static_cast<int>(HalDisplay::DISPLAY_HEIGHT);
const int width = std::min(hwW, hwH);
const int height = std::max(hwW, hwH);
const int rowBytes = ((width + 31) / 32) * 4;
const int imageSize = rowBytes * height;
const int fileSize = 14 + 40 + 8 + imageSize;
const int dataOffset = 14 + 40 + 8;
FsFile coverBmp;
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
return false;
}
coverBmp.write('B');
coverBmp.write('M');
coverBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
uint32_t reserved = 0;
coverBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
coverBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
uint32_t dibHeaderSize = 40;
coverBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
int32_t bmpWidth = width;
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
int32_t bmpHeight = -height;
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
uint16_t planes = 1;
coverBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
uint16_t bitsPerPixel = 1;
coverBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
uint32_t compression = 0;
coverBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
coverBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
int32_t ppmX = 2835;
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
int32_t ppmY = 2835;
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
uint32_t colorsUsed = 2;
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
uint32_t colorsImportant = 2;
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
coverBmp.write(black, 4);
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00};
coverBmp.write(white, 4);
for (int y = 0; y < height; y++) {
std::vector<uint8_t> rowData(rowBytes, 0xFF);
const int scaledY = (y * width) / height;
const int thickness = 6;
for (int x = 0; x < width; x++) {
bool drawPixel = false;
if (std::abs(x - scaledY) <= thickness) drawPixel = true;
if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true;
if (drawPixel) {
const int byteIndex = x / 8;
const int bitIndex = 7 - (x % 8);
rowData[byteIndex] &= static_cast<uint8_t>(~(1 << bitIndex));
}
}
coverBmp.write(rowData.data(), rowBytes);
}
coverBmp.close();
LOG_DBG("EBP", "Generated invalid format cover BMP");
return true;
}
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
if (itemHref.empty()) {
LOG_DBG("EBP", "Failed to read item, empty href");

View File

@@ -56,6 +56,9 @@ class Epub {
std::string getThumbBmpPath() const;
std::string getThumbBmpPath(int height) const;
bool generateThumbBmp(int height) const;
bool generateInvalidFormatCoverBmp(bool cropped = false) const;
bool generateInvalidFormatThumbBmp(int height) const;
static bool isValidThumbnailBmp(const std::string& bmpPath);
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;

View File

@@ -274,11 +274,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
}
bool BookMetadataCache::cleanupTmpFiles() const {
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
Storage.remove((cachePath + tmpSpineBinFile).c_str());
const auto spineBinFile = cachePath + tmpSpineBinFile;
if (Storage.exists(spineBinFile.c_str())) {
Storage.remove(spineBinFile.c_str());
}
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
Storage.remove((cachePath + tmpTocBinFile).c_str());
const auto tocBinFile = cachePath + tmpTocBinFile;
if (Storage.exists(tocBinFile.c_str())) {
Storage.remove(tocBinFile.c_str());
}
return true;
}

View File

@@ -5,6 +5,7 @@
#include <algorithm>
#include <cmath>
#include <cstring>
#include <functional>
#include <limits>
#include <vector>
@@ -74,6 +75,80 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
}
// ---------------------------------------------------------------------------
// Direct-mapped word-width cache
//
// Avoids redundant getTextAdvanceX calls when the same (word, style, fontId)
// triple appears across paragraphs. A fixed-size static array is used so
// that heap allocation and fragmentation are both zero.
//
// Eviction policy: hash-direct mapping — a word always occupies the single
// slot determined by its hash; a collision simply overwrites that slot.
// This gives O(1) lookup (one hash + one memcmp) regardless of how full the
// cache is, avoiding the O(n) linear-scan overhead that causes a regression
// on corpora with many unique words (e.g. German compound-heavy text).
//
// Words longer than 23 bytes bypass the cache entirely — they are uncommon,
// unlikely to repeat verbatim, and exceed the fixed-width key buffer.
// ---------------------------------------------------------------------------
struct WordWidthCacheEntry {
char word[24]; // NUL-terminated; 23 usable bytes + terminator
int fontId;
uint16_t width;
uint8_t style; // EpdFontFamily::Style narrowed to one byte
bool valid; // false = slot empty (BSS-initialised to 0)
};
// Power-of-two size → slot selection via fast bitmask AND.
// 128 entries × 32 bytes = 4 KB in BSS; covers typical paragraph vocabulary
// with a low collision rate even for German compound-heavy prose.
static constexpr uint32_t WORD_WIDTH_CACHE_SIZE = 128;
static constexpr uint32_t WORD_WIDTH_CACHE_MASK = WORD_WIDTH_CACHE_SIZE - 1;
static WordWidthCacheEntry s_wordWidthCache[WORD_WIDTH_CACHE_SIZE];
// FNV-1a over the word bytes, then XOR-folded with fontId and style.
static uint32_t wordWidthCacheHash(const char* str, const size_t len, const int fontId, const uint8_t style) {
uint32_t h = 2166136261u; // FNV offset basis
for (size_t i = 0; i < len; ++i) {
h ^= static_cast<uint8_t>(str[i]);
h *= 16777619u; // FNV prime
}
h ^= static_cast<uint32_t>(fontId);
h *= 16777619u;
h ^= style;
return h;
}
// Returns the cached width for (word, style, fontId), measuring and caching
// on a miss. Appending a hyphen is not supported — those measurements are
// word-fragment lookups that will not repeat and must not pollute the cache.
static uint16_t cachedMeasureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
const EpdFontFamily::Style style) {
const size_t len = word.size();
if (len >= 24) {
return measureWordWidth(renderer, fontId, word, style);
}
const uint8_t styleByte = static_cast<uint8_t>(style);
const char* const wordCStr = word.c_str();
const uint32_t slot = wordWidthCacheHash(wordCStr, len, fontId, styleByte) & WORD_WIDTH_CACHE_MASK;
auto& e = s_wordWidthCache[slot];
if (e.valid && e.fontId == fontId && e.style == styleByte && memcmp(e.word, wordCStr, len + 1) == 0) {
return e.width; // O(1) cache hit
}
const uint16_t w = measureWordWidth(renderer, fontId, word, style);
memcpy(e.word, wordCStr, len + 1);
e.fontId = fontId;
e.width = w;
e.style = styleByte;
e.valid = true;
return w;
}
} // namespace
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
@@ -101,20 +176,18 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
applyParagraphIndent();
const int pageWidth = viewportWidth;
const int spaceWidth = renderer.getSpaceWidth(fontId, EpdFontFamily::REGULAR);
auto wordWidths = calculateWordWidths(renderer, fontId);
std::vector<size_t> lineBreakIndices;
if (hyphenationEnabled) {
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
} else {
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
}
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) {
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
}
// Remove consumed words so size() reflects only remaining words
@@ -131,22 +204,25 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
wordWidths.reserve(words.size());
for (size_t i = 0; i < words.size(); ++i) {
wordWidths.push_back(measureWordWidth(renderer, fontId, words[i], wordStyles[i]));
wordWidths.push_back(cachedMeasureWordWidth(renderer, fontId, words[i], wordStyles[i]));
}
return wordWidths;
}
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
const int spaceWidth, std::vector<uint16_t>& wordWidths,
std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) {
if (words.empty()) {
return {};
}
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
// Calculate first line indent (only for left/justified text).
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
// it is structural (positions the bullet/marker), not decorative.
const int firstLineIndent =
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
@@ -184,9 +260,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
// Add space before word j, unless it's the first word on the line or a continuation
int gap = 0;
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
gap = spaceWidth;
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]),
wordStyles[j - 1]);
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]),
wordStyles[j - 1]);
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
@@ -238,6 +313,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
// Stores the index of the word that starts the next line (last_word_index + 1)
std::vector<size_t> lineBreakIndices;
lineBreakIndices.reserve(totalWordCount / 8 + 1);
size_t currentWordIndex = 0;
while (currentWordIndex < totalWordCount) {
@@ -272,12 +348,15 @@ void ParsedText::applyParagraphIndent() {
// Builds break indices while opportunistically splitting the word that would overflow the current line.
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
const int pageWidth, const int spaceWidth,
const int pageWidth,
std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) {
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
// Calculate first line indent (only for left/justified text).
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
// it is structural (positions the bullet/marker), not decorative.
const int firstLineIndent =
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
@@ -298,9 +377,8 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
const bool isFirstWord = currentIndex == lineStart;
int spacing = 0;
if (!isFirstWord && !continuesVec[currentIndex]) {
spacing = spaceWidth;
spacing += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[currentIndex - 1]),
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
} else if (!isFirstWord && continuesVec[currentIndex]) {
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
@@ -370,8 +448,11 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
size_t chosenOffset = 0;
int chosenWidth = -1;
bool chosenNeedsHyphen = true;
std::string prefix;
prefix.reserve(word.size());
// Iterate over each legal breakpoint and retain the widest prefix that still fits.
// Breakpoints are in ascending order, so once a prefix is too wide, all subsequent ones will be too.
for (const auto& info : breakInfos) {
const size_t offset = info.byteOffset;
if (offset == 0 || offset >= word.size()) {
@@ -379,9 +460,13 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
}
const bool needsHyphen = info.requiresInsertedHyphen;
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen);
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
continue; // Skip if too wide or not an improvement
prefix.assign(word, 0, offset);
const int prefixWidth = measureWordWidth(renderer, fontId, prefix, style, needsHyphen);
if (prefixWidth > availableWidth) {
break; // Ascending order: all subsequent breakpoints yield wider prefixes
}
if (prefixWidth <= chosenWidth) {
continue; // Not an improvement
}
chosenWidth = prefixWidth;
@@ -434,19 +519,21 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
return true;
}
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
const std::vector<size_t>& lineBreakIndices,
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
const GfxRenderer& renderer, const int fontId) {
const size_t lineBreak = lineBreakIndices[breakIndex];
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
// Calculate first line indent (only for left/justified text).
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
// it is structural (positions the bullet/marker), not decorative.
const bool isFirstLine = breakIndex == 0;
const int firstLineIndent =
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
@@ -462,8 +549,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// Count gaps: each word after the first creates a gap, unless it's a continuation
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
actualGapCount++;
int naturalGap = spaceWidth;
naturalGap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
int naturalGap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
firstCodepoint(words[lastBreakAt + wordIdx]),
wordStyles[lastBreakAt + wordIdx - 1]);
totalNaturalGaps += naturalGap;
@@ -485,8 +571,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
? spareSpace / static_cast<int>(actualGapCount)
: 0;
// Calculate initial x position (first line starts at indent for left/justified text)
auto xpos = static_cast<uint16_t>(firstLineIndent);
// Calculate initial x position (first line starts at indent for left/justified text;
// may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
auto xpos = static_cast<int16_t>(firstLineIndent);
if (blockStyle.alignment == CssTextAlign::Right) {
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
} else if (blockStyle.alignment == CssTextAlign::Center) {
@@ -495,7 +582,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// Pre-calculate X positions for words
// Continuation words attach to the previous word with no space before them
std::vector<uint16_t> lineXPos;
std::vector<int16_t> lineXPos;
lineXPos.reserve(lineWordCount);
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
@@ -510,12 +597,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
xpos += advance;
} else {
int gap = spaceWidth;
if (wordIdx + 1 < lineWordCount) {
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
wordStyles[lastBreakAt + wordIdx]);
}
int gap = wordIdx + 1 < lineWordCount
? renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
wordStyles[lastBreakAt + wordIdx])
: renderer.getSpaceWidth(fontId, wordStyles[lastBreakAt + wordIdx]);
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
gap += justifyExtra;
}

View File

@@ -21,14 +21,14 @@ class ParsedText {
bool hyphenationEnabled;
void applyParagraphIndent();
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
int spaceWidth, std::vector<uint16_t>& wordWidths,
std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec);
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
int fontId);

View File

@@ -4,16 +4,19 @@
#include <Logging.h>
#include <Serialization.h>
#include <algorithm>
#include <set>
#include "Epub/css/CssParser.h"
#include "Page.h"
#include "hyphenation/Hyphenator.h"
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 14;
constexpr uint8_t SECTION_FILE_VERSION = 19;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint32_t);
sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t);
} // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
@@ -36,7 +39,7 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool embeddedStyle) {
const bool embeddedStyle, const uint8_t imageRendering) {
if (!file) {
LOG_DBG("SCT", "File not open for writing header");
return;
@@ -44,7 +47,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(uint32_t),
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@@ -55,13 +58,16 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, viewportHeight);
serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
serialization::writePod(file, imageRendering);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
}
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering) {
if (!Storage.openFileForRead("SCT", filePath, file)) {
return false;
}
@@ -84,6 +90,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled;
bool fileEmbeddedStyle;
uint8_t fileImageRendering;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing);
@@ -92,11 +99,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering) {
file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache();
@@ -107,6 +116,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, pageCount);
file.close();
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
buildTocBoundaries(readAnchorMap(filePath));
return true;
}
@@ -129,7 +139,7 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const std::function<void()>& popupFn) {
const uint8_t imageRendering, const std::function<void()>& popupFn) {
const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@@ -179,7 +189,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle);
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering);
std::vector<uint32_t> lut = {};
// Derive the content base directory and image cache path prefix for the parser
@@ -197,11 +207,24 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
}
}
// Collect TOC anchors for this spine so the parser can insert page breaks at chapter boundaries
std::set<std::string> tocAnchors;
const int startTocIndex = epub->getTocIndexForSpineIndex(spineIndex);
if (startTocIndex >= 0) {
for (int i = startTocIndex; i < epub->getTocItemsCount(); i++) {
auto entry = epub->getTocItem(i);
if (entry.spineIndex != spineIndex) break;
if (!entry.anchor.empty()) {
tocAnchors.insert(std::move(entry.anchor));
}
}
}
ChapterHtmlSlimParser visitor(
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser);
Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages();
@@ -234,14 +257,31 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
// Go back and write LUT offset
file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount));
// Write anchor-to-page map for fragment navigation (footnotes + TOC)
const uint32_t anchorMapOffset = file.position();
const auto& anchors = visitor.getAnchors();
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
for (const auto& [anchor, page] : anchors) {
serialization::writeString(file, anchor);
serialization::writePod(file, page);
}
// Patch header with final pageCount, lutOffset, and anchorMapOffset
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2 - sizeof(pageCount));
serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset);
file.close();
if (cssParser) {
cssParser->clear();
}
// Convert anchor vector to map for buildTocBoundaries
std::map<std::string, uint16_t> anchorMap;
for (const auto& [a, p] : anchors) {
anchorMap.emplace(a, p);
}
buildTocBoundaries(anchorMap);
return true;
}
@@ -250,7 +290,7 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
return nullptr;
}
file.seek(HEADER_SIZE - sizeof(uint32_t));
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
uint32_t lutOffset;
serialization::readPod(file, lutOffset);
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
@@ -262,3 +302,170 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
file.close();
return page;
}
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
FsFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt;
}
const uint32_t fileSize = f.size();
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t anchorMapOffset;
serialization::readPod(f, anchorMapOffset);
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
f.close();
return std::nullopt;
}
f.seek(anchorMapOffset);
uint16_t count;
serialization::readPod(f, count);
for (uint16_t i = 0; i < count; i++) {
std::string key;
uint16_t page;
serialization::readString(f, key);
serialization::readPod(f, page);
if (key == anchor) {
f.close();
return page;
}
}
f.close();
return std::nullopt;
}
std::map<std::string, uint16_t> Section::readAnchorMap(const std::string& sectionPath) {
FsFile f;
if (!Storage.openFileForRead("SCT", sectionPath, f)) {
return {};
}
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t anchorMapOffset;
serialization::readPod(f, anchorMapOffset);
if (anchorMapOffset == 0) {
f.close();
return {};
}
f.seek(anchorMapOffset);
uint16_t count;
serialization::readPod(f, count);
std::map<std::string, uint16_t> result;
for (uint16_t i = 0; i < count; i++) {
std::string key;
uint16_t page;
serialization::readString(f, key);
serialization::readPod(f, page);
result.emplace(std::move(key), page);
}
f.close();
return result;
}
void Section::buildTocBoundaries(const std::map<std::string, uint16_t>& anchorMap) {
tocBoundaries.clear();
const int startTocIndex = epub->getTocIndexForSpineIndex(spineIndex);
if (startTocIndex < 0) return;
const int tocCount = epub->getTocItemsCount();
for (int i = startTocIndex; i < tocCount; i++) {
const auto entry = epub->getTocItem(i);
if (entry.spineIndex != spineIndex) break;
uint16_t page = 0;
if (!entry.anchor.empty()) {
auto it = anchorMap.find(entry.anchor);
if (it != anchorMap.end()) page = it->second;
}
tocBoundaries.push_back({i, page});
}
std::sort(tocBoundaries.begin(), tocBoundaries.end(),
[](const TocBoundary& a, const TocBoundary& b) { return a.startPage < b.startPage; });
}
int Section::getTocIndexForPage(const int page) const {
if (tocBoundaries.empty()) {
return epub->getTocIndexForSpineIndex(spineIndex);
}
auto it = std::upper_bound(tocBoundaries.begin(), tocBoundaries.end(), static_cast<uint16_t>(page),
[](uint16_t p, const TocBoundary& boundary) { return p < boundary.startPage; });
if (it == tocBoundaries.begin()) {
return tocBoundaries[0].tocIndex;
}
return std::prev(it)->tocIndex;
}
std::optional<int> Section::getPageForTocIndex(const int tocIndex) const {
for (const auto& boundary : tocBoundaries) {
if (boundary.tocIndex == tocIndex) {
return boundary.startPage;
}
}
return std::nullopt;
}
std::optional<Section::TocPageRange> Section::getPageRangeForTocIndex(const int tocIndex) const {
for (size_t i = 0; i < tocBoundaries.size(); i++) {
if (tocBoundaries[i].tocIndex == tocIndex) {
const int startPage = tocBoundaries[i].startPage;
const int endPage = (i + 1 < tocBoundaries.size()) ? static_cast<int>(tocBoundaries[i + 1].startPage) : pageCount;
return TocPageRange{startPage, endPage};
}
}
return std::nullopt;
}
std::optional<uint16_t> Section::readCachedPageCount(const std::string& cachePath, const int spineIndex,
const int fontId, const float lineCompression,
const bool extraParagraphSpacing, const uint8_t paragraphAlignment,
const uint16_t viewportWidth, const uint16_t viewportHeight,
const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering) {
const std::string path = cachePath + "/sections/" + std::to_string(spineIndex) + ".bin";
FsFile f;
if (!Storage.openFileForRead("SCT", path, f)) {
return std::nullopt;
}
uint8_t version;
serialization::readPod(f, version);
if (version != SECTION_FILE_VERSION) {
f.close();
return std::nullopt;
}
int fileFontId;
float fileLineCompression;
bool fileExtraParagraphSpacing;
uint8_t fileParagraphAlignment;
uint16_t fileViewportWidth, fileViewportHeight;
bool fileHyphenationEnabled, fileEmbeddedStyle;
uint8_t fileImageRendering;
serialization::readPod(f, fileFontId);
serialization::readPod(f, fileLineCompression);
serialization::readPod(f, fileExtraParagraphSpacing);
serialization::readPod(f, fileParagraphAlignment);
serialization::readPod(f, fileViewportWidth);
serialization::readPod(f, fileViewportHeight);
serialization::readPod(f, fileHyphenationEnabled);
serialization::readPod(f, fileEmbeddedStyle);
serialization::readPod(f, fileImageRendering);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering) {
f.close();
return std::nullopt;
}
uint16_t count;
serialization::readPod(f, count);
f.close();
return count;
}

View File

@@ -1,6 +1,10 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "Epub.h"
@@ -16,9 +20,18 @@ class Section {
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
bool embeddedStyle);
bool embeddedStyle, uint8_t imageRendering);
uint32_t onPageComplete(std::unique_ptr<Page> page);
struct TocBoundary {
int tocIndex = 0;
uint16_t startPage = 0;
};
std::vector<TocBoundary> tocBoundaries;
static std::map<std::string, uint16_t> readAnchorMap(const std::string& sectionPath);
void buildTocBoundaries(const std::map<std::string, uint16_t>& anchorMap);
public:
uint16_t pageCount = 0;
int currentPage = 0;
@@ -30,10 +43,31 @@ class Section {
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle);
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering);
bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
const std::function<void()>& popupFn = nullptr);
uint8_t imageRendering, const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file (used for footnotes).
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
// TOC boundary navigation: maps TOC entries to page ranges within this section.
int getTocIndexForPage(int page) const;
std::optional<int> getPageForTocIndex(int tocIndex) const;
struct TocPageRange {
int startPage;
int endPage;
};
std::optional<TocPageRange> getPageRangeForTocIndex(int tocIndex) const;
// Reads just the pageCount from an existing section cache file without loading the full section.
static std::optional<uint16_t> readCachedPageCount(const std::string& cachePath, int spineIndex, int fontId,
float lineCompression, bool extraParagraphSpacing,
uint8_t paragraphAlignment, uint16_t viewportWidth,
uint16_t viewportHeight, bool hyphenationEnabled,
bool embeddedStyle, uint8_t imageRendering);
};

29
lib/Epub/Epub/TableData.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <memory>
#include <vector>
#include "ParsedText.h"
#include "css/CssStyle.h"
/// A single cell in a table row.
struct TableCell {
std::unique_ptr<ParsedText> content;
bool isHeader = false; // true for <th>, false for <td>
int colspan = 1; // number of logical columns this cell spans
CssLength widthHint; // width hint from HTML attribute or CSS (if hasWidthHint)
bool hasWidthHint = false;
};
/// A single row in a table.
struct TableRow {
std::vector<TableCell> cells;
};
/// Buffered table data collected during SAX parsing.
/// The entire table must be buffered before layout because column widths
/// depend on content across all rows.
struct TableData {
std::vector<TableRow> rows;
std::vector<CssLength> colWidthHints; // width hints from <col> tags, indexed by logical column
};

View File

@@ -74,7 +74,7 @@ bool TextBlock::serialize(FsFile& file) const {
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
uint16_t wc;
std::vector<std::string> words;
std::vector<uint16_t> wordXpos;
std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles;
BlockStyle blockStyle;

View File

@@ -13,12 +13,12 @@
class TextBlock final : public Block {
private:
std::vector<std::string> words;
std::vector<uint16_t> wordXpos;
std::vector<int16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles;
BlockStyle blockStyle;
public:
explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
: words(std::move(words)),
wordXpos(std::move(word_xpos)),
@@ -28,6 +28,7 @@ class TextBlock final : public Block {
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
const BlockStyle& getBlockStyle() const { return blockStyle; }
const std::vector<std::string>& getWords() const { return words; }
const std::vector<int16_t>& getWordXpos() const { return wordXpos; }
bool isEmpty() override { return words.empty(); }
size_t wordCount() const { return words.size(); }
// given a renderer works out where to break the words into lines

View File

@@ -1,5 +1,6 @@
#include "JpegToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <JPEGDEC.h>
@@ -486,9 +487,5 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
}
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension;
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".jpg" || ext == ".jpeg");
return FsHelpers::hasJpgExtension(extension);
}

View File

@@ -1,5 +1,6 @@
#include "PngToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
@@ -391,9 +392,5 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension;
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".png");
return FsHelpers::hasPngExtension(extension);
}

View File

@@ -95,8 +95,6 @@ bool isPunctuation(const uint32_t cp) {
case '}':
case '[':
case ']':
case '/':
case 0x2039: //
case 0x203A: //
case 0x2026: // …
return true;
@@ -109,6 +107,7 @@ bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
bool isExplicitHyphen(const uint32_t cp) {
switch (cp) {
case '/':
case '-':
case 0x00AD: // soft hyphen
case 0x058A: // Armenian hyphen

Some files were not shown because too many files have changed in this diff Show More