Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio - #2802
Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio#2802mr-tbot wants to merge 21 commits into
Conversation
The UI chrome was designed for the 480x800-logical X4 canvas; the M3's canvas is 240x416 at ~130 PPI, so everything rendered twice as large as intended and key text was illegible. - New built-in UI font cuts for the murphy build only: ubuntu 7/8pt (Regular+Bold) and notosans 6pt, bound under the existing UI_10/UI_12/ SMALL font IDs so no call site changes. - ThemeMetrics density scale: each theme's constexpr metric set passes through scaledMetrics() (compile-time, kUiDensityScale=0.6 on murphy, 1.0 elsewhere). Touch-relevant heights floored at 28px (~5.5mm at 130 PPI). - Scaled the stray hardcoded layout constants (home margins, subtitleY bottom anchor, battery/list value widths, list subtitle offsets). - freeink-sdk submodule -> m3-tbot (OEM LUTs, touch power gate, I2C rail, SDMMC wiring). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MURPHY_CHARGE_PROBE builds log GPIO47 (the only unaccounted usable pin; TP4054 CHRG candidate, open-drain LOW while charging) plus the raw and scaled VBAT millivolts every second, and each touch tap's normalized + logical coordinates for the corner-tap mount-transform test. Remove the flag once the charge pin is identified and the transform is confirmed. Also pins freeink-sdk to the touch mount transform + OEM-only refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frontlight (Murphy M3 is the first CrossPoint device with one): - FrontlightManager (SDK) added to the murphy env's lib_deps - Persisted frontlightBrightness setting (0-100%, default off) - Settings -> Display -> "Frontlight": OptionPopup with 0/20/40/60/80/ 100%, applies live, persists, restored at boot; shown only when the board has a light; rides the shared list so the web settings API gets it too. All frontlight code behind __has_include guards so lightless envs compile unchanged. Charging bolt: HalGPIO::isUsbConnected() returned constant false with no VBUS-detect GPIO; it now falls back to freeink::usbHostPresent() (USB-Serial/JTAG SOF activity) on boards that opt in. The probe build now logs the exact UI-facing values (HAL battery %, USB state). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dark pages: every reader activity ran a grayscale plane sequence whose
final displayGray call hit PanelDriver's default on the Murphy driver —
refreshing the raw MSB plane as a frame (near-solid black). The SDK
driver now no-ops displayGray and reports supportsGrayscale()=false,
and all four call sites (EPUB, TXT renderAntiAliased, XTC, sleep cover)
skip building gray planes on B/W-only panels — pages render correctly
and page turns drop the wasted double re-render.
Reader fonts (murphy): new 9/10pt Noto Serif/Sans cuts (4 styles each),
selectable sizes {9,10,12,14} with 10pt default; the 16/18pt cuts are
compiled out on this build (physically enormous at 130 PPI). Flash
75.1% (was 80.8%). Other envs bit-identical.
Quick refresh: freeink-sdk restores the non-flashing FAST bank for page
turns/UI with every 7th refresh promoted to the OEM GC flash
(ghostClearInterval=6) now that the waveform tables are correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User feedback: 9pt still reads large on the 3.7" panel. Murphy's
selectable reader sizes become {5,6,7,8,9,10} with an 8pt default; the
12/14pt cuts leave this build like 16/18 before them (the always-
present notoserif14 default family had no murphy-side consumers beyond
the reader mapping — verified by grep). Legacy stored sizes snap to the
new list automatically. Flash 79.7%.
freeink-sdk pin: fast waveform runs its destination drive twice
(stronger latch, less residue per non-flashing refresh) and the GC
flash promotes every 5th refresh instead of every 7th.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ESP32-audioI2S 3.4.7 decodes + feeds I2S; SDK AudioManager handles ES8388 codec init and analog volume only (its I2S path unused). New lib/SdFatFS bridges HalStorage/HalFile to fs::FS for connecttoFS — every access stays behind the storage mutex. Dedicated decode task (16KB, core 0) pumps Audio::loop() under a mutex shared with UI control calls; EOF detected by polling isRunning() (callback-free, version-proof). Folder playlist with natural sort + auto-advance, tap-to-seek progress bar, prev/pause/next and volume touch rows, audioVolume setting (0-100, persisted on exit, settings + web API entry). Gated behind CROSSPOINT_AUDIO_PLAYER (murphy env only; needs AudioManager + ESP32-audioI2S in lib_deps). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lib/Mobi parses the PDB container, MOBI/EXTH headers and decompresses text records (none/PalmDOC LZ77/HUFF-CDIC, trailing-entry trim per extraDataFlags); DRM'd and pure-KF8 books are rejected with a reason. MobiToEpub tidies the HTML-3.2-ish markup into well-formed XHTML (tag whitelist with auto-close, named-entity mapping, cp1252->UTF-8, recindex image extraction, <mbp:pagebreak> chapter splits) and assembles an EPUB2 via new lib/ZipWriter (stored entries). Conversion is cached under /.crosspoint/mobi_<hash>/ keyed on text length, then the stock EPUB pipeline opens the book natively — TOC, page cache, progress and fonts unchanged. Reader dispatch, browser filter and file icon updated; not device-gated (works on all envs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MobiParser read the MOBI 'file version' at record offset 52 (correct: 36) and lastContentRecord at 194 (correct: 210, i.e. 0xC0/0xC2 from the MOBI header start), so real-world books failed as 'corrupt MOBI header'. Fixed and verified against a real Mobipocket v6 book via a host-side ASan harness (35 chapters, valid XHTML on every entry); every BadHeader path now logs its exact reason. New Audio section on the home screen (audio-capable builds): music-note icon, opens an audio-only file browser (FileBrowserActivity Mode::Audio) starting at /Audio, /Audiobooks or /Music when present. Browse Files no longer lists audio files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lib/Pdf converts text PDFs on-device: classic + stream xrefs (PNG/TIFF predictors), object streams, Flate/ASCIIHex/RunLength filters, page-tree walk with inheritance, content-stream interpreter (CTM/Tm state, TJ gap spaces, form XObject recursion), fonts via ToUnicode CMaps, WinAnsi/ MacRoman/Standard + /Differences (AGL subset), Identity-H CID. Output is a cached EPUB2 (10 pages per chapter) the stock reader opens natively. Host-verified under ASan/UBSan against classic-xref, xref-stream+ObjStm and 93-page real-world PDFs; encrypted and scanned PDFs fail with clear messages. Scanned/image PDFs and LZW/DCT content streams are out of scope for v1. Also: MOBI record table now accepts zero-length PDB records (legal padding/EOF markers — big builds like Bibles have them; previously rejected as corrupt); MobiToEpub tag parser could spin forever on a stray '/' inside a tag (found by fuzzing, 400 ASan iterations now pass); Classic-theme home menu caps the murphy cover tile so all rows fit and never paints rows off-panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on cache versioning Reader defaults on the M3 are now 7pt with tight leading; a one-shot migration pulls existing installs onto them (the old values were already persisted, so changing the default alone would never reach them) and the settings stay user-changeable afterwards. MOBI conversions now record a converter version in their cache tag. The generated EPUB keeps a stable path, so without this a stale conversion — and every reader section cache derived from it — survived a firmware upgrade. Image diagnostics for chasing missing illustrations: the converter logs any recindex it cannot extract and warns when a book stores GIF/BMP images (the reader only decodes JPEG and PNG); the chapter parser logs each image's src, resolved path, format support and probed dimensions under MURPHY_CHARGE_PROBE. Verified offline that conversion itself is correct: a 56-image book yields 33 referenced JPEGs whose decoded dimensions all match the dimensions the MOBI markup declares, in a ZIP that passes integrity and per-entry CRC checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ity) fontconvert quantizes 4-bit coverage to 2 bits and treats 1-3 as white. On a grayscale panel those pixels are invisible either way, but the M3 is 1-bit and paints every ink level black, so discarding them removed measured ~20% of the ink in a 7pt cut — thin strokes lost whole segments, which reads as patchy, uneven, washed-out text. New --ink-floor option (default 4, so every other device is unchanged) sets that cutoff; murphy's 5/6/7/8pt reader cuts and the ubuntu 7/8pt UI cuts are regenerated with --ink-floor 2. Glyph metrics are untouched (only quantization changed), so page layout and existing caches stay valid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Murphy M3 env only existed in a gitignored platformio.local.ini, so the device could not be built from a clean checkout. Moved into platformio.ini alongside the other device envs; the local file now carries only temporary debug instrumentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MOBI books commonly store illustrations as GIF, which the reader cannot decode, so those images silently vanished. GIFs are now decoded during conversion and re-encoded as PNG. lib/ImageTranscode gains a GIF87a/89a first-frame decoder (global and local colour tables, graphic-control transparency, de-interlacing, and an LZW reader whose bit stream walks sub-block boundaries so codes may straddle them) alongside the PNG encoder. The encoder emits stored deflate blocks because the vendored miniz has its compressor compiled out; that costs SD space, not CPU, and is paid once per image. The reader's PNG decoder ignores tRNS, so the transparent palette entry is forced to white before encoding — otherwise a transparent background renders as whatever colour that slot held, typically turning transparent artwork into a solid black block. tRNS is still emitted for decoders that honour it. Verified against 17 reference GIFs (interlaced, transparent, 256-colour, GIF87a, multi-frame, photographic) decoding pixel-identically to a reference decoder both directly and after the PNG round-trip, including sub-block re-chunking at sizes 1/2/3/7/254/255; 20 crafted malformed files fail cleanly; 400 fuzz iterations under sanitizers show no crashes or hangs; and a real book patched to carry GIF records converts with every image byte-correct and no .gif entries left behind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Illustrated and scanned PDFs previously converted to text-only books. Images are now emitted at their position in the reading order, resolved where the content interpreter already handles XObjects at Do, so images inside Form XObjects appear in the right place too. JPEG (DCTDecode) streams pass through byte-for-byte. Raw rasters are re-encoded as PNG: DeviceGray, DeviceRGB and Indexed (lookup as string or stream) at 1, 2, 4 and 8 bits per component, ICCBased treated as its component count, /Decode inversion and /ImageMask honoured, with RunLength/ASCIIHex rasters and Flate predictors working for free. The same XObject used on many pages is stored once and referenced many times. A document with no text but usable images now produces an image-only book instead of failing as a scanned PDF. Formats with no decoder on device (JPX, CCITT, LZW, CMYK, 16-bit, inline images) skip the individual image with one logged reason and never fail the document. Verified pixel-exact against reference decoders across every colour space and depth above, byte-identical for JPEG passthrough, reading-order and dedup checked explicitly, no text regression (19-page and 93-page PDFs extract the same character counts as before), and 620 fuzz iterations under sanitizers -- including 320 mutations that replace image dictionary values with hostile ones -- with no crashes or hangs. Known limit: Flate-encoded full-page 300dpi scans exceed the decoded raster cap and are skipped; the JPEG-encoded equivalent converts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The charge-pin hunt, tap-coordinate logging and image-pipeline diagnostics have served their purpose (the charge pin turned out not to exist -- host presence comes from the USB frame counter instead). Removed along with the MURPHY_CHARGE_PROBE flag they were gated on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A list band is rarely an exact multiple of the row height, so the leftover pixels below the last row belonged to no row and taps there were silently discarded. Whether a screen had that dead strip depended on its band height happening to divide evenly, which is why touch appeared to work in some menus and not others. It is worst on a short panel: with 28 px rows in a 354 px band the strip is 18 px, over half a row. The last drawn row now extends to the bottom of the band, in the shared list hit-test and in SettingsActivity's own copy of it. Taps genuinely below the list content are still ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified each against current code; fixed the still-valid ones. - scaledDim rounded negatives with a positive bias (add 0.5, truncate toward zero), so at scale 1.0 it silently shifted metrics on devices that do not scale -- keyboardVerticalOffset -13 became -12 and popupTextBaselineOffsetY -2 became -1. Now rounds half away from zero. - ByteBuf's defaulted moves copied len/cap while moving the pointer, leaving the moved-from buffer reporting a non-zero size over null data. - MobiParser::parseHuffCdic still used addition-form bounds (huffRecord + huffCount), which wraps; now subtraction-form like the rest of the parser. - appendUtf8 emitted malformed UTF-8 for code points past U+10FFFF, which a numeric character reference can name. - MOBI title/author replaced every non-ASCII byte with U+FFFD instead of decoding them like body text; they now share the UTF-8/cp1252 handling. - PDF metadata truncation at 256 bytes could split a UTF-8 sequence and make the OPF malformed; it now trims back to a boundary. - An unfiltered PDF stream allocated its declared length before the maxOut check. Encoded streams may legitimately exceed maxOut and are still checked after decoding. - Converted-book caches validated only the converter version plus a size or text length, so a replaced source could be served the previous book. Both now also compare a sampled content fingerprint (FsHelpers::sourceFingerprint), and the MOBI converter version is bumped so existing caches regenerate. - Inline-image notice was process-static (silent after the first conversion) and logged at error level; now per-interpreter and LOG_DBG. - Audio task stop/done flags are std::atomic rather than volatile, since they cross cores. - An overfull home menu logs what it clipped instead of silently hiding reachable rows; no current theme and device combination overflows. - Dead code removed (unused usedImages member, unreachable imageExt helper, unused encoding locals); platformio.ini comments sit with their own env again. Validated: firmware builds clean; both host harnesses re-run under AddressSanitizer and UndefinedBehaviorSanitizer -- three MOBI books (including the GIF-illustrated one) and two PDFs convert with valid ZIPs and XML, 400 MOBI fuzz iterations pass, and cache validation now detects 7/7 single-byte edits in a 148 KB source while still hitting the cache for an unchanged file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Floor 2 recovered too much: of the stroke ink in a 7pt glyph the original cut painted 79%, floor 2 painted 90% and read thick on the panel. Floor 3 paints 85% -- it keeps the genuine stroke-edge coverage that was leaving thin strokes patchy, and drops the two faintest levels that were fattening everything. Confirmed on the device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.agent/ holds working notes and per-file backups, and backup/ holds stock flash dumps whose NVS partition contains the device owner's WiFi credentials. Neither belongs in version control. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Replaces #2794, which I closed accidentally: I rewrote the branch to drop some agent working notes that had been committed by mistake, and the rewrite touched the upstream commits too, so the branch lost its common ancestry and GitHub closed the PR. This branch is the same work rebuilt correctly — only my own commits were rewritten, upstream history is untouched, and the Content is otherwise identical to #2794 at its final state, including the review fixes and the hardware-confirmed font tuning. |
📝 WalkthroughWalkthroughThis PR adds Murphy M3 support, device-specific fonts and UI scaling, audio playback, MOBI/PDF-to-EPUB conversion, GIF/PNG and ZIP infrastructure, grayscale capability gating, frontlight settings, and related reader/browser routing. ChangesMurphy device, fonts, and display behavior
Media and document support
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.45.0)lib/EpdFont/builtinFonts/all.hast-grep did not scan this file: retry isolation stopped after a systemic timeout or after exhausting the retry isolation budget lib/EpdFont/builtinFonts/notosans_10_bold.hast-grep did not scan this file: retry isolation stopped after a systemic timeout or after exhausting the retry isolation budget lib/EpdFont/builtinFonts/notosans_10_bolditalic.hast-grep did not scan this file: retry isolation stopped after a systemic timeout or after exhausting the retry isolation budget
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (6)
src/activities/audio/AudioPlayerActivity.cpp (1)
27-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider deleting copy/move on
MutexLock.Simple hardening: prevent accidental copies of the RAII guard from double-releasing the semaphore in future edits.
🛡️ Proposed hardening
class MutexLock { public: explicit MutexLock(SemaphoreHandle_t m) : m_(m) { xSemaphoreTake(m_, portMAX_DELAY); } ~MutexLock() { xSemaphoreGive(m_); } + MutexLock(const MutexLock&) = delete; + MutexLock& operator=(const MutexLock&) = delete; private: SemaphoreHandle_t m_; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/activities/audio/AudioPlayerActivity.cpp` around lines 27 - 34, Delete the copy and move constructors and assignment operators for MutexLock so the RAII guard cannot be copied or moved and accidentally release the semaphore multiple times. Keep its existing constructor and destructor behavior unchanged.lib/Mobi/MobiParser.cpp (1)
82-91: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRecord list uses a throwing allocation (~512KB worst case).
Everything else in this parser routes file-controlled sizes through capped/fallible allocation, but
std::vector<uint8_t> list(numRecords * 8)can request ~512KB andabort()on failure under-fno-exceptions. A nothrow buffer keeps the failure path consistent withreadRecord().Based on learnings: never let untrusted/large lengths reach a throwing allocation under
-fno-exceptions; prefer a fallible, bounded allocation with an explicit error.🛡️ Suggested fallible allocation
- std::vector<uint8_t> list(numRecords * 8); - if (file->read(list.data(), list.size()) != (int)list.size()) { + const size_t listBytes = (size_t)numRecords * 8; + auto list = makeUniqueNoThrow<uint8_t[]>(listBytes); + if (!list) { + err = Error::Oom; + return false; + } + if (file->read(list.get(), listBytes) != (int)listBytes) {(
#include <Memory.h>and update thebe32(&list[i * 8])call accordingly.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/Mobi/MobiParser.cpp` around lines 82 - 91, Replace the throwing std::vector allocation in the record-list parsing flow with the established fallible, bounded buffer allocation used by readRecord(), including the required Memory.h dependency. Handle allocation failure by setting err to Error::BadHeader and returning false, and update the be32 access in the record-offset loop to use the new buffer type.Source: Learnings
lib/Pdf/PdfDoc.cpp (1)
463-475: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNested
loadObjectinvalidates the windowlxis parsing.Resolving an indirect
/Lengthre-entersloadObjectAt, which callswindow.setSize()and can reallocate the very bufferlx(andwindow.data()[sp]) point into. It only works today because nothing window-derived is read after this block. That's an easy trap for the next edit — read the bytes you need first and make the dependency explicit.♻️ Make the constraint explicit
uint32_t slen = 0; if (const PdfObj* L = out.find("Length")) { if (L->isNum()) { slen = L->num > 0 ? (uint32_t)L->num : 0; } else if (L->kind == Kind::Ref && depth < 6) { + // Re-entrant: this reallocates `window`, so `lx` and any pointer into + // window.data() must not be used past this point. PdfObj lo; if (loadObject(L->ref, lo, depth + 1) && lo.isNum() && lo.num > 0) slen = (uint32_t)lo.num; } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/Pdf/PdfDoc.cpp` around lines 463 - 475, In the stream-length handling within loadObjectAt, preserve any required window-derived state before the nested loadObject(L->ref, lo, depth + 1) call, since that call may resize or reallocate the parsing window. Refactor the block so subsequent calculations use stable local values rather than lx or window.data()-backed pointers after recursive resolution, while preserving the existing length clamping and streamLen behavior.lib/EpdFont/scripts/fontconvert.py (1)
25-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueWrap this long help string for line-length compatibility.
Argparse may auto-format messages onto multiple lines, but the source line is long enough to trigger line-length linters enabled for Python files. Use an implicitly-concatenated help string or move the help text elsewhere.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/EpdFont/scripts/fontconvert.py` at line 25, Wrap the long help text in the argparse registration for the ink_floor option using adjacent implicitly concatenated string literals or another local formatting approach, while preserving the existing message and behavior.Source: Learnings
lib/FsHelpers/FsHelpers.cpp (1)
221-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocument the file-cursor side effect.
sourceFingerprintleavesfileseeked at an arbitrary offset (or wherever a failed seek/read left it). Any caller that reuses the sameHalFilefor parsing must re-seek. Worth stating in the header doc, or restoring the entry position before returning.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/FsHelpers/FsHelpers.cpp` around lines 221 - 246, Document in the header for sourceFingerprint that it changes the HalFile cursor and does not preserve the caller’s position, requiring callers to re-seek before reuse. Keep the current fingerprinting behavior unchanged.lib/SdFatFS/SdFatFS.cpp (1)
65-73: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueMode string is only inspected at
mode[0], so"a"truncates.
"a"(append) routes toopenFileForWrite, and"r+"/"w+"collapse to read/write-truncate. Harmless for the current read-only audio consumer, but a futurefs::FSuser opening in append mode would silently lose data. Consider rejecting unsupported modes explicitly rather than approximating them.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/SdFatFS/SdFatFS.cpp` around lines 65 - 73, Update open to validate the complete mode string instead of checking only mode[0]. Support only the modes that Storage.openFileForRead and Storage.openFileForWrite can represent, and reject append, read/write, and other unsupported combinations by returning nullptr without opening or truncating the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h`:
- Around line 3406-3412: Fix the font generator to derive kerning class counts
from the actual left/right class array sizes, then regenerate both affected
headers: lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h at lines 3406-3412
must use 552/558, and lib/EpdFont/builtinFonts/notosans_10_bolditalic.h at lines
3730-3736 must use 472/472, ensuring the final class entries are included in
lookup bounds.
In `@lib/ImageTranscode/PngWriter.cpp`:
- Around line 33-43: The encode implementation does not honor its documented
allocation-failure behavior because idat.reserve() and out.insert() can abort
without exceptions. Update encode to perform a worst-case non-throwing
allocation probe using makeUniqueNoThrow, following the GifDecoder.cpp pattern,
and return false when the probe fails before proceeding with buffer allocations.
In `@lib/Mobi/MobiToEpub.cpp`:
- Around line 602-648: Move the CHAPTER_HARD_LIMIT check in the MobiToEpub
text-processing flow so it executes before the entity branches in the & handling
logic, ensuring every appended text byte—including decoded character
references—can trigger b.forceSplit(). Preserve the existing entity decoding and
heading-text behavior while removing the bypass caused by early continue
statements.
In `@lib/Pdf/PdfDoc.cpp`:
- Around line 356-362: Clamp every conversion from PdfObj::num (double) to
uint32_t in parseTrailerDict and the other identified parsing sites around the
/Size, /XRefStm, /Index, and /Length handling to the uint32_t range before
casting. Reuse a shared finite-and-bounded conversion helper so values such as
1e300 cannot trigger undefined behavior under UBSan, while preserving normal
in-range values.
In `@lib/Pdf/PdfFont.cpp`:
- Around line 361-393: Restrict the codeBytes widening logic in the CMap parsing
branches around begincodespacerange, beginbfchar, and beginbfrange to Type0
fonts only. Preserve single-byte decoding for simple fonts even when the
ToUnicode CMap contains two-byte hex codes, while retaining the existing
two-byte behavior for Type0 fonts.
In `@lib/Pdf/PdfObject.h`:
- Around line 26-29: Update PdfObject::asInt() to validate the parser-controlled
numeric value before conversion: reject non-finite values and values outside the
int32_t range, returning the established failure representation or propagating
an error instead of clamping. Preserve valid integer and real conversions, and
update callers as needed so malformed PDF numbers are rejected before an int32_t
is returned.
In `@lib/ZipWriter/ZipWriter.cpp`:
- Around line 30-40: Update ZipWriter::begin to reset the streaming state to
false alongside offset and entries when starting a new archive, ensuring
subsequent addFile, beginStreamedFile, and finish operations work after an
abandoned streamed conversion.
In `@src/activities/audio/AudioPlayerActivity.cpp`:
- Around line 148-173: Update AudioPlayerActivity::buildPlaylist so initialPath
is always included and selected even when the directory contains more than
PLAYLIST_MAX audio files. Preserve the playlist cap for other entries, but
reserve space for or explicitly retain initialPath when it is encountered or
absent after scanning, then sort and assign trackIndex to its matching entry; do
not rely on the existing empty-playlist fallback.
In `@src/activities/settings/SettingsActivity.cpp`:
- Around line 207-212: Update the row-clamping logic in the settings pagination
flow so every row greater than or equal to rowsOnPage is clamped to rowsOnPage -
1, including rows beyond the final populated row on a partial page. Preserve the
existing rowsOnPage <= 0 early return and normal in-range row behavior.
In `@src/CrossPointSettings.h`:
- Around line 101-106: Update the Murphy-specific comment above
DEFAULT_FONT_POINT_SIZE to state that 7pt is the comfortable default, matching
the constant and the murphyReaderDefaultsV2 value. Leave the non-Murphy branch
unchanged.
- Around line 266-270: Update the documentation for
CrossPointSettings::frontlightBrightness to state that brightness values use UI
steps of 20, matching FRONTLIGHT_UI_STEP_PERCENT and buildFrontlightSetting()
while retaining the existing 0–100 percent range.
---
Nitpick comments:
In `@lib/EpdFont/scripts/fontconvert.py`:
- Line 25: Wrap the long help text in the argparse registration for the
ink_floor option using adjacent implicitly concatenated string literals or
another local formatting approach, while preserving the existing message and
behavior.
In `@lib/FsHelpers/FsHelpers.cpp`:
- Around line 221-246: Document in the header for sourceFingerprint that it
changes the HalFile cursor and does not preserve the caller’s position,
requiring callers to re-seek before reuse. Keep the current fingerprinting
behavior unchanged.
In `@lib/Mobi/MobiParser.cpp`:
- Around line 82-91: Replace the throwing std::vector allocation in the
record-list parsing flow with the established fallible, bounded buffer
allocation used by readRecord(), including the required Memory.h dependency.
Handle allocation failure by setting err to Error::BadHeader and returning
false, and update the be32 access in the record-offset loop to use the new
buffer type.
In `@lib/Pdf/PdfDoc.cpp`:
- Around line 463-475: In the stream-length handling within loadObjectAt,
preserve any required window-derived state before the nested loadObject(L->ref,
lo, depth + 1) call, since that call may resize or reallocate the parsing
window. Refactor the block so subsequent calculations use stable local values
rather than lx or window.data()-backed pointers after recursive resolution,
while preserving the existing length clamping and streamLen behavior.
In `@lib/SdFatFS/SdFatFS.cpp`:
- Around line 65-73: Update open to validate the complete mode string instead of
checking only mode[0]. Support only the modes that Storage.openFileForRead and
Storage.openFileForWrite can represent, and reject append, read/write, and other
unsupported combinations by returning nullptr without opening or truncating the
file.
In `@src/activities/audio/AudioPlayerActivity.cpp`:
- Around line 27-34: Delete the copy and move constructors and assignment
operators for MutexLock so the RAII guard cannot be copied or moved and
accidentally release the semaphore multiple times. Keep its existing constructor
and destructor behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ea7c7a41-3e60-47fe-b54b-b169ece87dd3
📒 Files selected for processing (124)
.gitignoreFIRMWARE-SHA256.txtfreeink-sdklib/EpdFont/builtinFonts/all.hlib/EpdFont/builtinFonts/notosans_10_bold.hlib/EpdFont/builtinFonts/notosans_10_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_italic.hlib/EpdFont/builtinFonts/notosans_10_regular.hlib/EpdFont/builtinFonts/notosans_5_bold.hlib/EpdFont/builtinFonts/notosans_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_5_italic.hlib/EpdFont/builtinFonts/notosans_5_regular.hlib/EpdFont/builtinFonts/notosans_6_bold.hlib/EpdFont/builtinFonts/notosans_6_bolditalic.hlib/EpdFont/builtinFonts/notosans_6_italic.hlib/EpdFont/builtinFonts/notosans_6_regular.hlib/EpdFont/builtinFonts/notosans_7_bold.hlib/EpdFont/builtinFonts/notosans_7_bolditalic.hlib/EpdFont/builtinFonts/notosans_7_italic.hlib/EpdFont/builtinFonts/notosans_7_regular.hlib/EpdFont/builtinFonts/notosans_8_bold.hlib/EpdFont/builtinFonts/notosans_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_8_italic.hlib/EpdFont/builtinFonts/notosans_8_regular.hlib/EpdFont/builtinFonts/notosans_9_bold.hlib/EpdFont/builtinFonts/notosans_9_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_italic.hlib/EpdFont/builtinFonts/notosans_9_regular.hlib/EpdFont/builtinFonts/notoserif_10_bold.hlib/EpdFont/builtinFonts/notoserif_10_bolditalic.hlib/EpdFont/builtinFonts/notoserif_10_italic.hlib/EpdFont/builtinFonts/notoserif_10_regular.hlib/EpdFont/builtinFonts/notoserif_5_bold.hlib/EpdFont/builtinFonts/notoserif_5_bolditalic.hlib/EpdFont/builtinFonts/notoserif_5_italic.hlib/EpdFont/builtinFonts/notoserif_5_regular.hlib/EpdFont/builtinFonts/notoserif_6_bold.hlib/EpdFont/builtinFonts/notoserif_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_italic.hlib/EpdFont/builtinFonts/notoserif_6_regular.hlib/EpdFont/builtinFonts/notoserif_7_bold.hlib/EpdFont/builtinFonts/notoserif_7_bolditalic.hlib/EpdFont/builtinFonts/notoserif_7_italic.hlib/EpdFont/builtinFonts/notoserif_7_regular.hlib/EpdFont/builtinFonts/notoserif_8_bold.hlib/EpdFont/builtinFonts/notoserif_8_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_italic.hlib/EpdFont/builtinFonts/notoserif_8_regular.hlib/EpdFont/builtinFonts/notoserif_9_bold.hlib/EpdFont/builtinFonts/notoserif_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_9_italic.hlib/EpdFont/builtinFonts/notoserif_9_regular.hlib/EpdFont/builtinFonts/ubuntu_7_bold.hlib/EpdFont/builtinFonts/ubuntu_7_regular.hlib/EpdFont/builtinFonts/ubuntu_8_bold.hlib/EpdFont/builtinFonts/ubuntu_8_regular.hlib/EpdFont/scripts/fontconvert.pylib/FsHelpers/FsHelpers.cpplib/FsHelpers/FsHelpers.hlib/GfxRenderer/GfxRenderer.cpplib/GfxRenderer/GfxRenderer.hlib/I18n/translations/english.yamllib/ImageTranscode/GifDecoder.cpplib/ImageTranscode/GifDecoder.hlib/ImageTranscode/PngWriter.cpplib/ImageTranscode/PngWriter.hlib/Mobi/MobiParser.cpplib/Mobi/MobiParser.hlib/Mobi/MobiToEpub.cpplib/Mobi/MobiToEpub.hlib/Pdf/PdfDoc.cpplib/Pdf/PdfDoc.hlib/Pdf/PdfFont.cpplib/Pdf/PdfFont.hlib/Pdf/PdfImage.cpplib/Pdf/PdfImage.hlib/Pdf/PdfLexer.cpplib/Pdf/PdfLexer.hlib/Pdf/PdfObject.hlib/Pdf/PdfText.cpplib/Pdf/PdfText.hlib/Pdf/PdfToEpub.cpplib/Pdf/PdfToEpub.hlib/Pdf/PdfUtil.hlib/SdFatFS/SdFatFS.cpplib/SdFatFS/SdFatFS.hlib/ZipWriter/ZipWriter.cpplib/ZipWriter/ZipWriter.hlib/hal/HalDisplay.cpplib/hal/HalDisplay.hlib/hal/HalGPIO.cppplatformio.inisdkconfig.murphysrc/CrossPointSettings.cppsrc/CrossPointSettings.hsrc/MappedInputManager.cppsrc/ReaderFontSizes.hsrc/SettingsList.hsrc/activities/ActivityManager.cppsrc/activities/ActivityManager.hsrc/activities/audio/AudioPlayerActivity.cppsrc/activities/audio/AudioPlayerActivity.hsrc/activities/boot_sleep/SleepActivity.cppsrc/activities/home/FileBrowserActivity.cppsrc/activities/home/FileBrowserActivity.hsrc/activities/home/HomeActivity.cppsrc/activities/home/HomeActivity.hsrc/activities/reader/EpubReaderActivity.cppsrc/activities/reader/ReaderActivity.cppsrc/activities/reader/ReaderActivity.hsrc/activities/reader/ReaderUtils.hsrc/activities/reader/XtcReaderActivity.cppsrc/activities/settings/SettingsActivity.cppsrc/components/UITheme.cppsrc/components/icons/music.hsrc/components/icons/music24.hsrc/components/themes/BaseTheme.cppsrc/components/themes/BaseTheme.hsrc/components/themes/lyra/Lyra3CoversTheme.hsrc/components/themes/lyra/LyraTheme.cppsrc/components/themes/lyra/LyraTheme.hsrc/components/themes/roundedraff/RoundedRaffTheme.hsrc/fontIds.hsrc/main.cpp
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
- GitHub Check: cppcheck
- GitHub Check: build
⚠️ CI failures not shown inline (2)
GitHub Actions: PR Formatting / 0_Title Check.txt: Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio
Conclusion: failure
##[group]Run amannn/action-semantic-pull-request@v6
with:
githubBaseUrl: https://api.github.com
env:
GITHUB_***REDACTED_SECRET_ASSIGNMENT***
##[endgroup]
##[error]No release type found in pull request title "Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/
GitHub Actions: PR Formatting / Title Check: Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio
Conclusion: failure
##[group]Run amannn/action-semantic-pull-request@v6
with:
githubBaseUrl: https://api.github.com
env:
GITHUB_***REDACTED_SECRET_ASSIGNMENT***
##[endgroup]
##[error]No release type found in pull request title "Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/
🧰 Additional context used
🧠 Learnings (21)
📚 Learning: 2026-02-26T05:59:54.187Z
Learnt from: znelson
Repo: crosspoint-reader/crosspoint-reader PR: 1168
File: lib/EpdFont/builtinFonts/opendyslexic_10_bold.h:2553-2553
Timestamp: 2026-02-26T05:59:54.187Z
Learning: Generated font header files in lib/EpdFont/builtinFonts/*.h are produced by the script convert-builtin-fonts.sh and must not be edited by hand; any manual edits will be overwritten. When making changes, update the source inputs or the script and re-run the generation process, and avoid committing edits to these headers.
Applied to files:
lib/EpdFont/builtinFonts/ubuntu_8_bold.hlib/EpdFont/builtinFonts/notosans_8_regular.hlib/EpdFont/builtinFonts/notosans_5_italic.hlib/EpdFont/builtinFonts/notosans_7_regular.hlib/EpdFont/builtinFonts/all.hlib/EpdFont/builtinFonts/notosans_7_italic.hlib/EpdFont/builtinFonts/notoserif_7_bold.hlib/EpdFont/builtinFonts/notosans_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bold.hlib/EpdFont/builtinFonts/notosans_9_italic.hlib/EpdFont/builtinFonts/notoserif_10_regular.hlib/EpdFont/builtinFonts/notosans_8_bold.hlib/EpdFont/builtinFonts/notosans_8_italic.hlib/EpdFont/builtinFonts/notoserif_7_regular.hlib/EpdFont/builtinFonts/notoserif_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_regular.hlib/EpdFont/builtinFonts/notosans_6_bold.hlib/EpdFont/builtinFonts/notosans_7_bolditalic.hlib/EpdFont/builtinFonts/notoserif_9_regular.hlib/EpdFont/builtinFonts/notoserif_9_italic.hlib/EpdFont/builtinFonts/notosans_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_5_regular.hlib/EpdFont/builtinFonts/notosans_6_italic.hlib/EpdFont/builtinFonts/notosans_10_regular.hlib/EpdFont/builtinFonts/notoserif_9_bold.hlib/EpdFont/builtinFonts/notoserif_10_bold.hlib/EpdFont/builtinFonts/notoserif_5_bold.hlib/EpdFont/builtinFonts/notosans_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_italic.hlib/EpdFont/builtinFonts/ubuntu_8_regular.hlib/EpdFont/builtinFonts/notosans_10_bold.hlib/EpdFont/builtinFonts/ubuntu_7_bold.hlib/EpdFont/builtinFonts/notoserif_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_regular.hlib/EpdFont/builtinFonts/notoserif_5_italic.hlib/EpdFont/builtinFonts/notosans_7_bold.hlib/EpdFont/builtinFonts/ubuntu_7_regular.hlib/EpdFont/builtinFonts/notosans_9_bold.hlib/EpdFont/builtinFonts/notosans_5_bold.hlib/EpdFont/builtinFonts/notosans_6_regular.hlib/EpdFont/builtinFonts/notoserif_10_bolditalic.hlib/EpdFont/builtinFonts/notoserif_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_italic.hlib/EpdFont/builtinFonts/notoserif_8_italic.hlib/EpdFont/builtinFonts/notoserif_6_bold.hlib/EpdFont/builtinFonts/notoserif_5_regular.hlib/EpdFont/builtinFonts/notoserif_8_regular.hlib/EpdFont/builtinFonts/notoserif_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_bolditalic.h
📚 Learning: 2026-02-26T05:59:54.187Z
Learnt from: znelson
Repo: crosspoint-reader/crosspoint-reader PR: 1168
File: lib/EpdFont/builtinFonts/opendyslexic_10_bold.h:2553-2553
Timestamp: 2026-02-26T05:59:54.187Z
Learning: Guideline: Do not modify generated header files directly (e.g., lib/EpdFont/builtinFonts/*.h) to fix font metrics. If space-width issues from upstream fonts arise, implement the mitigation in the upstream font processing step (fontconvert.py), such as optional clamping or substituting normative em-fractions for specific Unicode spaces (e.g., U+205F). This ensures headers remain as generated and changes are traceable through the generation process.
Applied to files:
lib/EpdFont/builtinFonts/ubuntu_8_bold.hlib/EpdFont/builtinFonts/notosans_8_regular.hlib/EpdFont/builtinFonts/notosans_5_italic.hlib/EpdFont/builtinFonts/notosans_7_regular.hlib/EpdFont/builtinFonts/all.hlib/EpdFont/builtinFonts/notosans_7_italic.hlib/EpdFont/builtinFonts/notoserif_7_bold.hlib/EpdFont/builtinFonts/notosans_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bold.hlib/EpdFont/builtinFonts/notosans_9_italic.hlib/EpdFont/builtinFonts/notoserif_10_regular.hlib/EpdFont/builtinFonts/notosans_8_bold.hlib/EpdFont/builtinFonts/notosans_8_italic.hlib/EpdFont/builtinFonts/notoserif_7_regular.hlib/EpdFont/builtinFonts/notoserif_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_regular.hlib/EpdFont/builtinFonts/notosans_6_bold.hlib/EpdFont/builtinFonts/notosans_7_bolditalic.hlib/EpdFont/builtinFonts/notoserif_9_regular.hlib/EpdFont/builtinFonts/notoserif_9_italic.hlib/EpdFont/builtinFonts/notosans_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_5_regular.hlib/EpdFont/builtinFonts/notosans_6_italic.hlib/EpdFont/builtinFonts/notosans_10_regular.hlib/EpdFont/builtinFonts/notoserif_9_bold.hlib/EpdFont/builtinFonts/notoserif_10_bold.hlib/EpdFont/builtinFonts/notoserif_5_bold.hlib/EpdFont/builtinFonts/notosans_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_italic.hlib/EpdFont/builtinFonts/ubuntu_8_regular.hlib/EpdFont/builtinFonts/notosans_10_bold.hlib/EpdFont/builtinFonts/ubuntu_7_bold.hlib/EpdFont/builtinFonts/notoserif_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_regular.hlib/EpdFont/builtinFonts/notoserif_5_italic.hlib/EpdFont/builtinFonts/notosans_7_bold.hlib/EpdFont/builtinFonts/ubuntu_7_regular.hlib/EpdFont/builtinFonts/notosans_9_bold.hlib/EpdFont/builtinFonts/notosans_5_bold.hlib/EpdFont/builtinFonts/notosans_6_regular.hlib/EpdFont/builtinFonts/notoserif_10_bolditalic.hlib/EpdFont/builtinFonts/notoserif_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_italic.hlib/EpdFont/builtinFonts/notoserif_8_italic.hlib/EpdFont/builtinFonts/notoserif_6_bold.hlib/EpdFont/builtinFonts/notoserif_5_regular.hlib/EpdFont/builtinFonts/notoserif_8_regular.hlib/EpdFont/builtinFonts/notoserif_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_bolditalic.h
📚 Learning: 2026-04-12T12:28:33.205Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 1629
File: src/activities/home/HomeActivity.cpp:119-120
Timestamp: 2026-04-12T12:28:33.205Z
Learning: When reviewing code in this repository (C/C++ sources), set review comment severity according to this policy:
- Use **Major** (🟠) only for defects with realistic risk of crash, out-of-memory (OOM), invalid pointer dereference, data corruption, or other severe issues that are unlikely to be caught in casual/manual device testing.
- Use **Minor** or informational for UX gaps, logic edge-cases, style issues, or missing feature completeness that the author can verify (or has verified) through normal device use.
- Do **not** escalate severity to Major based on behavioral/UX observations alone; assume the author has already tested the feature on their own device and only treat issues as Major if they match the high-risk defect categories above.
Applied to files:
lib/EpdFont/builtinFonts/ubuntu_8_bold.hlib/EpdFont/builtinFonts/notosans_8_regular.hsrc/components/icons/music.hlib/Mobi/MobiToEpub.hlib/SdFatFS/SdFatFS.hlib/hal/HalDisplay.hlib/GfxRenderer/GfxRenderer.cpplib/hal/HalDisplay.cppsrc/activities/ActivityManager.cpplib/ImageTranscode/GifDecoder.hsrc/components/icons/music24.hsrc/activities/home/FileBrowserActivity.cpplib/Pdf/PdfToEpub.hsrc/components/themes/lyra/Lyra3CoversTheme.hsrc/activities/boot_sleep/SleepActivity.cpplib/Pdf/PdfFont.hsrc/MappedInputManager.cppsrc/activities/home/FileBrowserActivity.hsrc/components/UITheme.cpplib/GfxRenderer/GfxRenderer.hlib/hal/HalGPIO.cppsrc/ReaderFontSizes.hlib/Pdf/PdfText.hlib/Pdf/PdfImage.hsrc/activities/audio/AudioPlayerActivity.hsrc/activities/reader/EpubReaderActivity.cppsrc/activities/reader/ReaderUtils.hlib/Pdf/PdfLexer.hlib/EpdFont/builtinFonts/notosans_5_italic.hsrc/activities/settings/SettingsActivity.cpplib/SdFatFS/SdFatFS.cpplib/EpdFont/builtinFonts/notosans_7_regular.hsrc/components/themes/lyra/LyraTheme.hsrc/activities/ActivityManager.hlib/EpdFont/builtinFonts/all.hlib/Pdf/PdfUtil.hlib/EpdFont/builtinFonts/notosans_7_italic.hlib/Pdf/PdfObject.hsrc/activities/reader/ReaderActivity.hlib/ImageTranscode/GifDecoder.cpplib/ZipWriter/ZipWriter.hlib/ImageTranscode/PngWriter.hlib/Mobi/MobiParser.hsrc/SettingsList.hlib/EpdFont/builtinFonts/notoserif_7_bold.hsrc/CrossPointSettings.hsrc/activities/home/HomeActivity.hsrc/components/themes/BaseTheme.cpplib/EpdFont/builtinFonts/notosans_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bold.hsrc/fontIds.hsrc/components/themes/BaseTheme.hlib/Pdf/PdfImage.cpplib/EpdFont/builtinFonts/notosans_9_italic.hlib/Pdf/PdfText.cpplib/EpdFont/builtinFonts/notoserif_10_regular.hlib/ImageTranscode/PngWriter.cpplib/EpdFont/builtinFonts/notosans_8_bold.hlib/EpdFont/builtinFonts/notosans_8_italic.hsrc/CrossPointSettings.cpplib/EpdFont/builtinFonts/notoserif_7_regular.hlib/EpdFont/builtinFonts/notoserif_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_regular.hlib/EpdFont/builtinFonts/notosans_6_bold.hlib/EpdFont/builtinFonts/notosans_7_bolditalic.hlib/EpdFont/builtinFonts/notoserif_9_regular.hlib/EpdFont/builtinFonts/notoserif_9_italic.hlib/EpdFont/builtinFonts/notosans_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_5_regular.hlib/Pdf/PdfLexer.cpplib/EpdFont/builtinFonts/notosans_6_italic.hlib/Mobi/MobiToEpub.cppsrc/activities/home/HomeActivity.cpplib/EpdFont/builtinFonts/notosans_10_regular.hlib/EpdFont/builtinFonts/notoserif_9_bold.hlib/EpdFont/builtinFonts/notoserif_10_bold.hlib/FsHelpers/FsHelpers.hlib/EpdFont/builtinFonts/notoserif_5_bold.hlib/EpdFont/builtinFonts/notosans_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_italic.hsrc/activities/reader/XtcReaderActivity.cpplib/EpdFont/builtinFonts/notoserif_7_italic.hlib/EpdFont/builtinFonts/ubuntu_8_regular.hlib/Pdf/PdfFont.cpplib/Pdf/PdfDoc.hlib/Mobi/MobiParser.cpplib/ZipWriter/ZipWriter.cpplib/Pdf/PdfToEpub.cppsrc/components/themes/roundedraff/RoundedRaffTheme.hlib/EpdFont/builtinFonts/notosans_10_bold.hlib/EpdFont/builtinFonts/ubuntu_7_bold.hlib/EpdFont/builtinFonts/notoserif_9_bolditalic.hsrc/activities/reader/ReaderActivity.cpplib/EpdFont/builtinFonts/notoserif_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_regular.hlib/EpdFont/builtinFonts/notoserif_5_italic.hlib/EpdFont/builtinFonts/notosans_7_bold.hlib/EpdFont/builtinFonts/ubuntu_7_regular.hlib/EpdFont/builtinFonts/notosans_9_bold.hlib/FsHelpers/FsHelpers.cppsrc/activities/audio/AudioPlayerActivity.cpplib/EpdFont/builtinFonts/notosans_5_bold.hlib/EpdFont/builtinFonts/notosans_6_regular.hlib/EpdFont/builtinFonts/notoserif_10_bolditalic.hlib/EpdFont/builtinFonts/notoserif_5_bolditalic.hsrc/main.cpplib/EpdFont/builtinFonts/notosans_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_italic.hlib/EpdFont/builtinFonts/notoserif_8_italic.hlib/Pdf/PdfDoc.cpplib/EpdFont/builtinFonts/notoserif_6_bold.hlib/EpdFont/builtinFonts/notoserif_5_regular.hlib/EpdFont/builtinFonts/notoserif_8_regular.hlib/EpdFont/builtinFonts/notoserif_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_bolditalic.hsrc/components/themes/lyra/LyraTheme.cpplib/EpdFont/builtinFonts/notosans_10_bolditalic.h
📚 Learning: 2026-05-24T21:10:19.897Z
Learnt from: jeremydk
Repo: crosspoint-reader/crosspoint-reader PR: 2076
File: src/network/HttpDownloader.cpp:166-171
Timestamp: 2026-05-24T21:10:19.897Z
Learning: Follow the repo’s CLAUDE.md guidance: avoid adding error handling, fallbacks, or extra validation for scenarios that are provably unreachable given existing internal invariants and framework guarantees. Only add validation/guards at true system boundaries (e.g., user input, external APIs/network/IPC). For internal C++ APIs like HttpDownloader, do not add defensive checks against misuse (e.g., an empty std::function callback) when there is no reachable call site that can supply such values—guards in those cases are explicitly discouraged.
Applied to files:
lib/EpdFont/builtinFonts/ubuntu_8_bold.hlib/EpdFont/builtinFonts/notosans_8_regular.hsrc/components/icons/music.hlib/Mobi/MobiToEpub.hlib/SdFatFS/SdFatFS.hlib/hal/HalDisplay.hlib/GfxRenderer/GfxRenderer.cpplib/hal/HalDisplay.cppsrc/activities/ActivityManager.cpplib/ImageTranscode/GifDecoder.hsrc/components/icons/music24.hsrc/activities/home/FileBrowserActivity.cpplib/Pdf/PdfToEpub.hsrc/components/themes/lyra/Lyra3CoversTheme.hsrc/activities/boot_sleep/SleepActivity.cpplib/Pdf/PdfFont.hsrc/MappedInputManager.cppsrc/activities/home/FileBrowserActivity.hsrc/components/UITheme.cpplib/GfxRenderer/GfxRenderer.hlib/hal/HalGPIO.cppsrc/ReaderFontSizes.hlib/Pdf/PdfText.hlib/Pdf/PdfImage.hsrc/activities/audio/AudioPlayerActivity.hsrc/activities/reader/EpubReaderActivity.cppsrc/activities/reader/ReaderUtils.hlib/Pdf/PdfLexer.hlib/EpdFont/builtinFonts/notosans_5_italic.hsrc/activities/settings/SettingsActivity.cpplib/SdFatFS/SdFatFS.cpplib/EpdFont/builtinFonts/notosans_7_regular.hsrc/components/themes/lyra/LyraTheme.hsrc/activities/ActivityManager.hlib/EpdFont/builtinFonts/all.hlib/Pdf/PdfUtil.hlib/EpdFont/builtinFonts/notosans_7_italic.hlib/Pdf/PdfObject.hsrc/activities/reader/ReaderActivity.hlib/ImageTranscode/GifDecoder.cpplib/ZipWriter/ZipWriter.hlib/ImageTranscode/PngWriter.hlib/Mobi/MobiParser.hsrc/SettingsList.hlib/EpdFont/builtinFonts/notoserif_7_bold.hsrc/CrossPointSettings.hsrc/activities/home/HomeActivity.hsrc/components/themes/BaseTheme.cpplib/EpdFont/builtinFonts/notosans_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bold.hsrc/fontIds.hsrc/components/themes/BaseTheme.hlib/Pdf/PdfImage.cpplib/EpdFont/builtinFonts/notosans_9_italic.hlib/Pdf/PdfText.cpplib/EpdFont/builtinFonts/notoserif_10_regular.hlib/ImageTranscode/PngWriter.cpplib/EpdFont/builtinFonts/notosans_8_bold.hlib/EpdFont/builtinFonts/notosans_8_italic.hsrc/CrossPointSettings.cpplib/EpdFont/builtinFonts/notoserif_7_regular.hlib/EpdFont/builtinFonts/notoserif_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_regular.hlib/EpdFont/builtinFonts/notosans_6_bold.hlib/EpdFont/builtinFonts/notosans_7_bolditalic.hlib/EpdFont/builtinFonts/notoserif_9_regular.hlib/EpdFont/builtinFonts/notoserif_9_italic.hlib/EpdFont/builtinFonts/notosans_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_5_regular.hlib/Pdf/PdfLexer.cpplib/EpdFont/builtinFonts/notosans_6_italic.hlib/Mobi/MobiToEpub.cppsrc/activities/home/HomeActivity.cpplib/EpdFont/builtinFonts/notosans_10_regular.hlib/EpdFont/builtinFonts/notoserif_9_bold.hlib/EpdFont/builtinFonts/notoserif_10_bold.hlib/FsHelpers/FsHelpers.hlib/EpdFont/builtinFonts/notoserif_5_bold.hlib/EpdFont/builtinFonts/notosans_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_italic.hsrc/activities/reader/XtcReaderActivity.cpplib/EpdFont/builtinFonts/notoserif_7_italic.hlib/EpdFont/builtinFonts/ubuntu_8_regular.hlib/Pdf/PdfFont.cpplib/Pdf/PdfDoc.hlib/Mobi/MobiParser.cpplib/ZipWriter/ZipWriter.cpplib/Pdf/PdfToEpub.cppsrc/components/themes/roundedraff/RoundedRaffTheme.hlib/EpdFont/builtinFonts/notosans_10_bold.hlib/EpdFont/builtinFonts/ubuntu_7_bold.hlib/EpdFont/builtinFonts/notoserif_9_bolditalic.hsrc/activities/reader/ReaderActivity.cpplib/EpdFont/builtinFonts/notoserif_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_regular.hlib/EpdFont/builtinFonts/notoserif_5_italic.hlib/EpdFont/builtinFonts/notosans_7_bold.hlib/EpdFont/builtinFonts/ubuntu_7_regular.hlib/EpdFont/builtinFonts/notosans_9_bold.hlib/FsHelpers/FsHelpers.cppsrc/activities/audio/AudioPlayerActivity.cpplib/EpdFont/builtinFonts/notosans_5_bold.hlib/EpdFont/builtinFonts/notosans_6_regular.hlib/EpdFont/builtinFonts/notoserif_10_bolditalic.hlib/EpdFont/builtinFonts/notoserif_5_bolditalic.hsrc/main.cpplib/EpdFont/builtinFonts/notosans_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_italic.hlib/EpdFont/builtinFonts/notoserif_8_italic.hlib/Pdf/PdfDoc.cpplib/EpdFont/builtinFonts/notoserif_6_bold.hlib/EpdFont/builtinFonts/notoserif_5_regular.hlib/EpdFont/builtinFonts/notoserif_8_regular.hlib/EpdFont/builtinFonts/notoserif_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_bolditalic.hsrc/components/themes/lyra/LyraTheme.cpplib/EpdFont/builtinFonts/notosans_10_bolditalic.h
📚 Learning: 2026-07-28T18:42:02.364Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 2772
File: lib/Epub/Epub/blocks/TextBlock.cpp:454-458
Timestamp: 2026-07-28T18:42:02.364Z
Learning: When deserializing untrusted data in C++ (e.g., via Serialization.h/serialization::readString), never read an untrusted uint32_t length and pass it directly to std::string::resize. Under -fno-exceptions, allocation failure can abort the process. Implement (and use) a centrally-available fallible, bounded readString API that validates the requested byte length before allocation—preferably by checking it against bytes remaining in the input/file. Migrate all call sites that deserialize strings (e.g., cache readers for metadata such as spine/TOC, image blocks, section anchor maps, and TextBlock ruby <rt> text) to use the new safe API. Do not introduce a fixed maximum “read-only” content cap unless the corresponding writer/parser also enforces the same constraint; otherwise current unbounded ruby <rt> content can cause cache rebuild loops due to mismatch between parse-time and read-time limits.
Applied to files:
lib/EpdFont/builtinFonts/ubuntu_8_bold.hlib/EpdFont/builtinFonts/notosans_8_regular.hlib/Mobi/MobiToEpub.hlib/SdFatFS/SdFatFS.hlib/hal/HalDisplay.hlib/GfxRenderer/GfxRenderer.cpplib/hal/HalDisplay.cpplib/ImageTranscode/GifDecoder.hlib/Pdf/PdfToEpub.hlib/Pdf/PdfFont.hlib/GfxRenderer/GfxRenderer.hlib/hal/HalGPIO.cpplib/Pdf/PdfText.hlib/Pdf/PdfImage.hlib/Pdf/PdfLexer.hlib/EpdFont/builtinFonts/notosans_5_italic.hlib/SdFatFS/SdFatFS.cpplib/EpdFont/builtinFonts/notosans_7_regular.hlib/EpdFont/builtinFonts/all.hlib/Pdf/PdfUtil.hlib/EpdFont/builtinFonts/notosans_7_italic.hlib/Pdf/PdfObject.hlib/ImageTranscode/GifDecoder.cpplib/ZipWriter/ZipWriter.hlib/ImageTranscode/PngWriter.hlib/Mobi/MobiParser.hlib/EpdFont/builtinFonts/notoserif_7_bold.hlib/EpdFont/builtinFonts/notosans_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bold.hlib/Pdf/PdfImage.cpplib/EpdFont/builtinFonts/notosans_9_italic.hlib/Pdf/PdfText.cpplib/EpdFont/builtinFonts/notoserif_10_regular.hlib/ImageTranscode/PngWriter.cpplib/EpdFont/builtinFonts/notosans_8_bold.hlib/EpdFont/builtinFonts/notosans_8_italic.hlib/EpdFont/builtinFonts/notoserif_7_regular.hlib/EpdFont/builtinFonts/notoserif_6_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_regular.hlib/EpdFont/builtinFonts/notosans_6_bold.hlib/EpdFont/builtinFonts/notosans_7_bolditalic.hlib/EpdFont/builtinFonts/notoserif_9_regular.hlib/EpdFont/builtinFonts/notoserif_9_italic.hlib/EpdFont/builtinFonts/notosans_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_5_regular.hlib/Pdf/PdfLexer.cpplib/EpdFont/builtinFonts/notosans_6_italic.hlib/Mobi/MobiToEpub.cpplib/EpdFont/builtinFonts/notosans_10_regular.hlib/EpdFont/builtinFonts/notoserif_9_bold.hlib/EpdFont/builtinFonts/notoserif_10_bold.hlib/FsHelpers/FsHelpers.hlib/EpdFont/builtinFonts/notoserif_5_bold.hlib/EpdFont/builtinFonts/notosans_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_italic.hlib/EpdFont/builtinFonts/ubuntu_8_regular.hlib/Pdf/PdfFont.cpplib/Pdf/PdfDoc.hlib/Mobi/MobiParser.cpplib/ZipWriter/ZipWriter.cpplib/Pdf/PdfToEpub.cpplib/EpdFont/builtinFonts/notosans_10_bold.hlib/EpdFont/builtinFonts/ubuntu_7_bold.hlib/EpdFont/builtinFonts/notoserif_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_8_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_regular.hlib/EpdFont/builtinFonts/notoserif_5_italic.hlib/EpdFont/builtinFonts/notosans_7_bold.hlib/EpdFont/builtinFonts/ubuntu_7_regular.hlib/EpdFont/builtinFonts/notosans_9_bold.hlib/FsHelpers/FsHelpers.cpplib/EpdFont/builtinFonts/notosans_5_bold.hlib/EpdFont/builtinFonts/notosans_6_regular.hlib/EpdFont/builtinFonts/notoserif_10_bolditalic.hlib/EpdFont/builtinFonts/notoserif_5_bolditalic.hlib/EpdFont/builtinFonts/notosans_9_bolditalic.hlib/EpdFont/builtinFonts/notoserif_6_italic.hlib/EpdFont/builtinFonts/notoserif_8_italic.hlib/Pdf/PdfDoc.cpplib/EpdFont/builtinFonts/notoserif_6_bold.hlib/EpdFont/builtinFonts/notoserif_5_regular.hlib/EpdFont/builtinFonts/notoserif_8_regular.hlib/EpdFont/builtinFonts/notoserif_10_italic.hlib/EpdFont/builtinFonts/notoserif_7_bolditalic.hlib/EpdFont/builtinFonts/notosans_10_bolditalic.h
📚 Learning: 2026-03-21T15:30:08.678Z
Learnt from: znelson
Repo: crosspoint-reader/crosspoint-reader PR: 1451
File: lib/EpdFont/FontDecompressor.cpp:248-252
Timestamp: 2026-03-21T15:30:08.678Z
Learning: In lib/EpdFont/FontDecompressor.cpp, do not flag FontDecompressor::prewarmCache() returning -1 (when all MAX_PAGE_SLOTS=4 page slots are exhausted) as an active bug if analysis shows it is unreachable in the intended flow: PrewarmScope must call clearCache() and then iterate at most MAX_PAGE_SLOTS font styles. Treat this -1 as a defensive guard against future misuse; if needed for reviewer clarity, ensure the -1 error case is documented (e.g., via a header/comment) rather than raising it as a live defect. Caller FontCacheManager::prewarmCache() should be considered correct as long as it only checks missed > 0, which covers the normal path.
Applied to files:
.gitignore
📚 Learning: 2026-04-09T19:53:14.958Z
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1488
File: src/activities/home/HomeActivity.cpp:73-77
Timestamp: 2026-04-09T19:53:14.958Z
Learning: In crosspoint-reader/crosspoint-reader’s src/activities/home/HomeActivity.cpp, ensure `coverDisabled` is ONLY cleared (set to `false`) at exactly two explicit locations: (1) immediately after a successful `generateThumbBmp()` call inside `HomeActivity::loadRecentCovers`, and (2) immediately after the explicit user-driven COVER_ACTION (“Enable cover” / “Generate cover”) handled in `EpubReaderActivity`. Do not clear `coverDisabled` in `HomeActivity::loadRecentCovers` or `HomeActivity::render` solely because a cached/thumbnail BMP exists on disk; a stale BMP must not override the per-book `coverDisabled` flag.
Applied to files:
.gitignore
📚 Learning: 2026-04-12T02:49:03.552Z
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1644
File: src/components/themes/BaseTheme.cpp:835-835
Timestamp: 2026-04-12T02:49:03.552Z
Learning: In this codebase, theme-specific keyboard key rendering may intentionally use small vertical offsets for secondary labels. In BaseTheme::drawKeyboardKey (src/components/themes/BaseTheme.cpp), the secondary label offset of -3 px (drawn at rect.y - 3) is deliberate and should not be flagged as a bug or requested to be aligned to rect.y. The offset is tied to BaseTheme’s keySpacing = 10 inter-row gap; other themes (e.g., LyraTheme with keySpacing = 0) may correctly use rect.y with no offset.
Applied to files:
.gitignore
📚 Learning: 2026-04-18T21:52:34.972Z
Learnt from: zgredex
Repo: crosspoint-reader/crosspoint-reader PR: 1614
File: lib/GfxRenderer/GfxRenderer.cpp:1491-1540
Timestamp: 2026-04-18T21:52:34.972Z
Learning: In crosspoint-reader/crosspoint-reader/lib/GfxRenderer/GfxRenderer.cpp, specifically GfxRenderer::displayXtchPlanes, the usual pre-flash step (clearScreen() + displayBuffer(HalDisplay::HALF_REFRESH)) must be treated as intentionally absent for the XTC plane-copy/bypass path. That path should drive the factory LUT directly and rely on the periodic FULL_REFRESH every 32 pages (pagesSinceClean in XtcReaderActivity) to reset DC balance, avoiding a per-page black flash on XTC pages. Do not flag the missing pre-flash in displayXtchPlanes as a bug in future reviews.
Applied to files:
.gitignore
📚 Learning: 2026-05-07T17:30:45.214Z
Learnt from: znelson
Repo: crosspoint-reader/crosspoint-reader PR: 1847
File: src/components/themes/BaseTheme.cpp:83-86
Timestamp: 2026-05-07T17:30:45.214Z
Learning: In this project’s theming code, do not require `BaseTheme::drawBatteryLeft` or `BaseTheme::drawBatteryRight` to perform their own pre-draw `fillRect`/clearing of the battery percentage text region. Their callers already clear the relevant area: `drawBatteryRight` is invoked from `drawHeader`, which fills the entire battery region before the call, and `drawBatteryLeft` is invoked from `drawStatusBar`, which runs after a full `clearScreen()`. Therefore, reviewers should avoid flagging missing pre-draw clearing inside these two methods as a ghost-pixel bug.
Applied to files:
.gitignore
📚 Learning: 2026-02-27T00:13:27.555Z
Learnt from: ngxson
Repo: crosspoint-reader/crosspoint-reader PR: 1212
File: lib/hal/HalStorage.h:50-52
Timestamp: 2026-02-27T00:13:27.555Z
Learning: In C++ header files, using a forward declaration like 'class StorageLock;' in the public section is valid only if the nested/implementation type is defined in the corresponding .cpp. This pattern keeps implementation details private while allowing the public API to reference the type. The comment 'private class, used internally' should refer to the implementation visibility, not the forward-declaration visibility. Ensure the implementation is complete in the .cpp and avoid leaking private types through header-only exposure.
Applied to files:
lib/hal/HalDisplay.h
📚 Learning: 2026-02-27T22:49:59.600Z
Learnt from: ngxson
Repo: crosspoint-reader/crosspoint-reader PR: 1218
File: src/activities/ActivityManager.cpp:254-265
Timestamp: 2026-02-27T22:49:59.600Z
Learning: In this codebase, assertions are always enabled (no NDEBUG). Use assert() to crash on programmer errors and surface logic bugs during development and in production builds. Do not rely on asserts for runtime error handling; they should enforce invariants that must always hold. Keep asserts side-effect free and inexpensive, and avoid relying on them for user-visible failures. Include <cassert> where appropriate and document the invariant being tested.
Applied to files:
lib/GfxRenderer/GfxRenderer.cpplib/hal/HalDisplay.cppsrc/activities/ActivityManager.cppsrc/activities/home/FileBrowserActivity.cppsrc/activities/boot_sleep/SleepActivity.cppsrc/MappedInputManager.cppsrc/components/UITheme.cpplib/hal/HalGPIO.cppsrc/activities/reader/EpubReaderActivity.cppsrc/activities/settings/SettingsActivity.cpplib/SdFatFS/SdFatFS.cpplib/ImageTranscode/GifDecoder.cppsrc/components/themes/BaseTheme.cpplib/Pdf/PdfImage.cpplib/Pdf/PdfText.cpplib/ImageTranscode/PngWriter.cppsrc/CrossPointSettings.cpplib/Pdf/PdfLexer.cpplib/Mobi/MobiToEpub.cppsrc/activities/home/HomeActivity.cppsrc/activities/reader/XtcReaderActivity.cpplib/Pdf/PdfFont.cpplib/Mobi/MobiParser.cpplib/ZipWriter/ZipWriter.cpplib/Pdf/PdfToEpub.cppsrc/activities/reader/ReaderActivity.cpplib/FsHelpers/FsHelpers.cppsrc/activities/audio/AudioPlayerActivity.cppsrc/main.cpplib/Pdf/PdfDoc.cppsrc/components/themes/lyra/LyraTheme.cpp
📚 Learning: 2026-03-02T10:14:16.036Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 1245
File: lib/Epub/Epub/Section.cpp:277-308
Timestamp: 2026-03-02T10:14:16.036Z
Learning: Guideline: Strengthen serialization::readString to defend against unbounded growth when reading from disk data. Implement and enforce a maximum allowed length (e.g., a configured or reasonable constant) and validate the incoming length before resizing or allocating. Audit all call sites (e.g., BookMetadataCache, TextBlock, KOReaderCredentialStore, Section cache readers) to ensure they do not rely on unbounded len-based resizing. If the readString API must remain, add internal safeguards (bounds checks, length validation, and error handling) so per-call-site validations are not required. Ensure Section cache files remain versioned (SECTION_FILE_VERSION) and parameter mismatches invalidate caches, but do not rely on unsafe allocations; prefer safe, bounded reads with explicit errors on invalid data.
Applied to files:
lib/GfxRenderer/GfxRenderer.cpplib/hal/HalDisplay.cppsrc/activities/ActivityManager.cppsrc/activities/home/FileBrowserActivity.cppsrc/activities/boot_sleep/SleepActivity.cppsrc/MappedInputManager.cppsrc/components/UITheme.cpplib/hal/HalGPIO.cppsrc/activities/reader/EpubReaderActivity.cppsrc/activities/settings/SettingsActivity.cpplib/SdFatFS/SdFatFS.cpplib/ImageTranscode/GifDecoder.cppsrc/components/themes/BaseTheme.cpplib/Pdf/PdfImage.cpplib/Pdf/PdfText.cpplib/ImageTranscode/PngWriter.cppsrc/CrossPointSettings.cpplib/Pdf/PdfLexer.cpplib/Mobi/MobiToEpub.cppsrc/activities/home/HomeActivity.cppsrc/activities/reader/XtcReaderActivity.cpplib/Pdf/PdfFont.cpplib/Mobi/MobiParser.cpplib/ZipWriter/ZipWriter.cpplib/Pdf/PdfToEpub.cppsrc/activities/reader/ReaderActivity.cpplib/FsHelpers/FsHelpers.cppsrc/activities/audio/AudioPlayerActivity.cppsrc/main.cpplib/Pdf/PdfDoc.cppsrc/components/themes/lyra/LyraTheme.cpp
📚 Learning: 2026-02-23T22:11:42.181Z
Learnt from: ariel-lindemann
Repo: crosspoint-reader/crosspoint-reader PR: 1133
File: lib/I18n/translations/finnish.yaml:92-92
Timestamp: 2026-02-23T22:11:42.181Z
Learning: In translation YAML files located in lib/I18n/translations/, literal percent signs (%) in strings (e.g., STR_HIDE_BATTERY, STR_GO_TO_PERCENT, STR_PERCENT_STEP_HINT) do not require escaping as %% and this convention is consistent across all language translations. Ensure that this pattern is followed for all YAML files in this directory to avoid introducing incorrect formatting markers in localized strings.
Applied to files:
lib/I18n/translations/english.yaml
📚 Learning: 2026-03-25T10:28:30.516Z
Learnt from: jpirnay
Repo: crosspoint-reader/crosspoint-reader PR: 1495
File: lib/I18n/translations/german.yaml:294-322
Timestamp: 2026-03-25T10:28:30.516Z
Learning: In crosspoint-reader, it is expected to include “pre-emptive” translation keys in non-English YAML files before the corresponding English keys exist (e.g., german.yaml ahead of en.yaml). When reviewing changes to files under lib/I18n/translations/*.yaml, do not flag missing corresponding English keys in non-English locales as an error if the repo’s gen_i18n.py fallback mechanism is intended to handle missing English keys safely until the pending PRs land.
Applied to files:
lib/I18n/translations/english.yaml
📚 Learning: 2026-04-17T18:59:20.878Z
Learnt from: KymAndriy
Repo: crosspoint-reader/crosspoint-reader PR: 1684
File: lib/I18n/translations/ukrainian.yaml:29-29
Timestamp: 2026-04-17T18:59:20.878Z
Learning: For Ukrainian (and other Slavic-language) translation YAML strings in lib/I18n/translations/*.yaml: if a format string contains a numeric placeholder produced via snprintf-style formatting (e.g., %zu) and runtime noun inflection is not feasible, place the relevant noun in genitive plural before the number in the string (e.g., "мереж %zu") rather than after it (e.g., "мережу %zu"). This avoids incorrect case forms that would otherwise result from the value affecting inflection (e.g., wrong forms like "мережу/мережі/мереж"). In such cases, do not flag the word order as incorrect.
Applied to files:
lib/I18n/translations/english.yaml
📚 Learning: 2026-02-19T17:46:26.871Z
Learnt from: Tritlo
Repo: crosspoint-reader/crosspoint-reader PR: 1003
File: src/activities/reader/EpubReaderActivity.cpp:657-674
Timestamp: 2026-02-19T17:46:26.871Z
Learning: In EpubReaderActivity.cpp, renderContents() behavior is intentional: if uncached images exist, Phase 1 should call displayBuffer(forceFullRefresh) to perform a HALF_REFRESH and clear ghosting; Phase 2 should call renderer.displayBuffer() without forceFullRefresh for a faster refresh. Grayscale anti-aliasing is applied separately after renderContents() via displayGrayBuffer(). This guidance applies specifically to this file and situation.
Applied to files:
src/activities/reader/EpubReaderActivity.cpp
📚 Learning: 2026-02-23T06:18:08.408Z
Learnt from: raygan
Repo: crosspoint-reader/crosspoint-reader PR: 1116
File: src/activities/settings/WallabagSettingsActivity.cpp:57-75
Timestamp: 2026-02-23T06:18:08.408Z
Learning: When navigating from a settings activity to KeyboardEntryActivity (e.g., KOReaderSettingsActivity, WallabagSettingsActivity), call exitActivity() on the parent before calling enterNewActivity(new KeyboardEntryActivity(...)). The KeyboardEntryActivity should then call exitActivity() via its callbacks to return to the parent. Note: ConfirmationActivity does not require exitActivity() before entering KeyboardEntryActivity. Apply this pattern to similar settings activities in this module.
Applied to files:
src/activities/settings/SettingsActivity.cpp
📚 Learning: 2026-02-23T19:55:03.054Z
Learnt from: martinbrook
Repo: crosspoint-reader/crosspoint-reader PR: 1136
File: scripts/patch_jpegdec.py:20-23
Timestamp: 2026-02-23T19:55:03.054Z
Learning: In the crosspoint-reader project, enable Python linters (e.g., Ruff or Flake8) in CI for all Python scripts. Run linting as part of the CI workflow and fix reported issues to maintain code quality. Do not skip linting for any Python files.
Applied to files:
lib/EpdFont/scripts/fontconvert.py
📚 Learning: 2026-03-25T01:15:04.951Z
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1488
File: src/components/themes/lyra/Lyra3CoversTheme.cpp:39-40
Timestamp: 2026-03-25T01:15:04.951Z
Learning: In crosspoint-reader/crosspoint-reader theme drawing code, `COVER_DISABLED_MODE` only controls whether the app generates new covers (e.g., on HOME entry) to avoid blocking the main thread. It must NOT be treated as a render-suppression switch for already-cached covers. Theme draw paths should only gate cached cover rendering on the per-book `RecentBook::coverDisabled` flag; if a cached cover BMP is available, the correct behavior is to render it even when `COVER_DISABLED_MODE` is set. Therefore, do not flag theme draw code for not checking `COVER_DISABLED_MODE`; checking only `coverDisabled` is correct by design.
Applied to files:
src/components/themes/BaseTheme.cppsrc/components/themes/lyra/LyraTheme.cpp
📚 Learning: 2026-04-17T23:57:16.108Z
Learnt from: pablohc
Repo: crosspoint-reader/crosspoint-reader PR: 1644
File: src/components/themes/lyra/LyraTheme.cpp:616-627
Timestamp: 2026-04-17T23:57:16.108Z
Learning: When reviewing theme rendering in crosspoint-reader (e.g., LyraTheme.cpp / BaseTheme.cpp), do not treat `invert = isSelected && !inactiveSelection` as a bug when the keyboard key is intentionally disabled (`keyType == KeyboardKeyType::Disabled`). For disabled keys, the implementation deliberately uses LightGray with white/inverted text because the renderer only supports black/white text; adding a `keyType != KeyboardKeyType::Disabled` guard would break the intended disabled-state visual indicator. Only flag changes if they alter the deliberate disabled rendering semantics.
Applied to files:
src/components/themes/BaseTheme.cppsrc/components/themes/lyra/LyraTheme.cpp
| notoserif_7_bolditalicKernLeftClasses, | ||
| notoserif_7_bolditalicKernRightClasses, | ||
| notoserif_7_bolditalicKernMatrix, | ||
| 551, | ||
| 557, | ||
| 103, | ||
| 90, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
for name in (
"lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h",
"lib/EpdFont/builtinFonts/notosans_10_bolditalic.h",
):
text = Path(name).read_text()
counts = []
for match in re.finditer(
r"static const EpdKernClassEntry \w+\[\] = \{(.*?)\n\};", text, re.S
):
counts.append(len(re.findall(r"^\s*\{\s*0x", match.group(1), re.M)))
print(f"{name}: kerning entry arrays = {counts}")
PY
fd -a '^EpdFontData\.h$' . -x rg -n -C3 'Kern|kern' {}Repository: crosspoint-reader/crosspoint-reader
Length of output: 2103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'pwd/check:'
pwd
printf '\nFiles matching notos fonts:\n'
fd -a 'notos(ans|serif).*bolditalic\.h$|EpdFontData\.h$|EpdFont.*Generator|Generator|epd.*font' . 2>/dev/null | head -100 || trueRepository: crosspoint-reader/crosspoint-reader
Length of output: 2074
🌐 Web query:
crosspoint-reader lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h notosans_10_bolditalic.h EpdFontData.h kerning counts
💡 Result:
In the crosspoint-reader project, kerning support was introduced as part of an overhaul to the font rendering system, which includes the EpdFontData structure defined in lib/EpdFont/EpdFontData.h [1][2]. This data structure was extended to store arrays of kern pairs and ligature pairs, along with their respective counts (e.g., kernPairCount and ligaturePairCount) [1]. Regarding the specific header files mentioned: - notoserif_7_bolditalic.h and notosans_10_bolditalic.h are examples of built-in font data files located in lib/EpdFont/builtinFonts/ [1]. - When these fonts are generated using the project's conversion scripts, kerning data is extracted from the source fonts and embedded directly into these headers [1]. - While specific "kerning counts" vary by font weight and style, data from similar font generation processes in the project indicate that typical kerning pair counts for Noto-style fonts often range from approximately 130,000 to over 150,000 extracted pairs (for instance, bold-italic variants often contain around 153,800 pairs, while regular variants may contain around 140,374 pairs) [3]. The project's font rendering engine uses these embedded tables for dynamic kerning adjustments during text layout [1]. Recent refactoring (as of early 2026) has transitioned the lookup routines—including lookupKernClass and getLigature—to use standard library binary search functions (std::lower_bound) to maintain efficient access to this data [4].
Citations:
- 1: feat: Support for kerning and ligatures #873
- 2: feat: Overhaul font format into CrossPoint font #589
- 3: Custom font generation failing on https://crosspointreader.com/ #2003
- 4: refactor: Use std binary search algorithms for font lookups #1202
🌐 Web query:
github crosspoint-reader lib/EpdFont/EpdFontData.h EpdFontData kernClassLeftCount kernClassRightCount
💡 Result:
The members kernClassLeftCount and kernClassRightCount are components of the EpdFontData structure found in the EpdFont library for the CrossPoint Reader firmware [1][2]. This library handles custom e-paper display fonts, and these specific fields are part of the font data definition used to manage kerning classes within the font format [1][3]. In the context of the CrossPoint Reader project, the EpdFont library provides structures and scripts to convert TrueType/OpenType fonts into a custom binary format (.cpfont) optimized for the device's e-paper display [2][4][5]. The kernClassLeftCount and kernClassRightCount variables are typically used to define the boundaries or sizes of kerning class tables within the font structure, facilitating efficient kerning lookups during text rendering [3]. If you are examining the codebase (such as in lib/EpdFont/EpdFontData.h), these fields help the firmware navigate the font's internal binary layout, which has been optimized for performance and flash memory usage [1][3][6]. Recent refactors in the project have focused on updating lookup functions—including those for kerning classes—to utilize standard library binary search algorithms like std::lower_bound, while preserving the original structure of the font data [3].
Citations:
- 1: https://github.com/zrn-ns/crosspoint-jp/tree/master/lib/EpdFont
- 2: https://github.com/crosspoint-reader/crosspoint-reader
- 3: refactor: Use std binary search algorithms for font lookups #1202
- 4: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/sd-card-fonts.md
- 5: https://github.com/crosspoint-reader/crosspoint-fonts
- 6: feat: Overhaul font format into CrossPoint font #589
🌐 Web query:
"notoserif_7_bolditalicKernLeftClasses" "notosans_10_bolditalicKernLeftClasses"
💡 Result:
The terms "notoserif_7_bolditalicKernLeftClasses" and "notosans_10_bolditalicKernLeftClasses" are not standard identifiers in official Noto font documentation [1][2][3] or widely recognized public technical specifications. Based on technical context regarding font development: 1. Font Development Context: Tools such as fontlab or ufo2ft (which generate OpenType 'kern' features) utilize "kerning classes" to manage spacing between groups of glyphs [4][5]. 2. Naming Conventions: In professional font engineering, internal class names often follow patterns like [FontFamily][Weight/Style][Attribute]_[Side]. The strings you provided appear to be programmatically generated or project-specific internal labels for kerning classes—likely referring to "Bold Italic" variants of Noto Serif or Noto Sans with specific kerning groups defined for left-side positioning [4][5]. 3. Availability: These are not user-facing CSS class names or font family names [2][6]. If you are encountering these, they are likely internal identifiers within a source font file (e.g., a UFO file) or a font-compilation tool configuration used during the build process of Noto fonts [5]. For authoritative documentation on Noto font naming, weights, and styles, refer to the official Noto documentation maintained by the Noto Fonts team [2][3].
Citations:
- 1: https://notofonts.github.io/noto-docs/specimen/NotoSerif/
- 2: https://github.com/notofonts/noto-docs/blob/main/docs/technical/Noto-Weight-Width-Style-Specification.md
- 3: https://notofonts.github.io/noto-docs/technical/Noto-Weight-Width-Style-Specification/
- 4: https://help.fontlab.com/fontlab/7/manual/Using-Kerning-Classes/
- 5: https://github.com/googlefonts/ufo2ft/blob/main/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
- 6: https://tools.bontragerconnection.com/NotoSans/
🌐 Web query:
github raw crosspoint-reader lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h lines 3406
💡 Result:
The file lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h is a generated header file used in the CrossPoint Reader firmware project [1][2]. These files are automatically produced by font conversion scripts (e.g., convert-builtin-fonts.sh) and are not intended to be edited by hand; any manual changes to them are overwritten during the build process [2]. Because these headers contain machine-generated font data—typically arrays representing glyph bitmaps and metadata—they are not standard human-readable code. Line 3406 in such a file would likely contain a portion of a font data array or structure definition, depending on the specific typeface and font size generated. As these files are dynamically generated from upstream font sources during the build process, the exact content of line 3406 is subject to change based on the specific version of the font generation script or the upstream font files being processed [1][2]. If you are looking to modify fonts in the CrossPoint Reader, the project recommends using the dedicated font conversion tools available in the repository (lib/EpdFont/scripts) rather than directly editing the built-in header files [1].
Citations:
- 1: https://github.com/crosspoint-reader/crosspoint-fonts
- 2: refactor: Use std binary search algorithms for font lookups #1202
🌐 Web query:
"static const EpdKernClassEntry" "kernPairCount" "lookupKernClass" crosspoint-reader
💡 Result:
The terms EpdKernClassEntry, lookupKernClass, and kernPairCount are components of the font rendering system used in CrossPoint Reader, an open-source e-reader firmware [1][2]. EpdKernClassEntry is a data structure used in the firmware's font format to define kerning classes, which optimize the storage of kerning information by grouping glyphs with similar spacing properties [3][4]. The font data structures typically include arrays of these entries (e.g., kernLeftClasses and kernRightClasses) alongside a flat kerning matrix that stores kerning values for class pairs [3][4]. lookupKernClass is a function within the firmware's font rendering engine (specifically found in lib/EpdFont/EpdFont.cpp) responsible for resolving kerning class indices for given glyphs [5]. As of early 2026, this function was refactored to utilize standard C++ library binary search algorithms, such as std::lower_bound, to efficiently perform these lookups while maintaining existing fallback and exact-match logic [5]. While kernPairCount is a variable name often associated with counting entries in kerning-related tables (such as ligature pair counts in similar structures) [3], it describes part of the broader metadata management for font metrics within the CrossPoint rendering engine [3][4]. The architecture is designed to handle compressed, custom font formats optimized for e-paper displays [3][2].
Citations:
- 1: https://github.com/daveallie/crosspoint-reader
- 2: feat: Overhaul font format into CrossPoint font #589
- 3: bcrpntr@7dc5186
- 4: bcrpntr@f1e9dc7
- 5: refactor: Use std binary search algorithms for font lookups #1202
🌐 Web query:
site:github.com/crosspoint-reader/crosspoint-reader "kernClassLeftCount" "kernClassRightCount"
💡 Result:
The terms kernClassLeftCount and kernClassRightCount are internal variables used within the crosspoint-reader software project to manage font kerning class data [1]. In the context of this codebase, they represent the number of entries or counts for kerning classes associated with the left and right sides of glyph pairs, respectively [1]. These variables are part of the font data structures used by the EpdFont and SdCardFont systems, which handle font rendering and kerning adjustments for e-paper displays [1][2]. Specifically: - They are used to track the capacity and current count of left-hand and right-hand kerning classes when building or loading font kerning matrices [1]. - They appear in memory management and initialization routines (e.g., buildMiniKernMatrix) where, instead of freeing and reallocating memory, the software resets these counts to zero to preserve allocated capacities for reuse and prevent heap fragmentation [1]. - They are part of a custom font format implementation that extracts kerning and ligature information from OpenType font files into a specialized, memory-efficient format optimized for the reader's firmware [2][3][4]. These are not standard OpenType specification terms, but rather specific implementation details of the crosspoint-reader project's font engine [1][2].
Citations:
- 1: feat: deferred refresh, memory work port, and first-open speedups #2611
- 2: feat: Support for kerning and ligatures #873
- 3: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/sd-card-fonts.md
- 4: feat: Overhaul font format into CrossPoint font #589
🌐 Web query:
"Kern" "built-in font" "convert-builtin-fonts.sh" crosspoint-reader
💡 Result:
The term "Kern" in the context of CrossPoint Reader refers to the implementation of typographic kerning support within the e-reader firmware [1][2]. CrossPoint Reader is an open-source firmware project for Xteink X3 and X4 e-paper devices [2][3]. The script convert-builtin-fonts.sh is a critical tool within the project's build pipeline, responsible for generating the built-in font header files (.h) used by the reader engine [1][4]. Key aspects of this process include: 1. Font Conversion and Generation: The script invokes fontconvert.py (or similar Python-based utilities) to convert standard TTF/OTF font files into a compressed, custom format optimized for the device's limited memory [1][5]. 2. Kerning and Ligature Support: Recent updates to the firmware have introduced support for extracting kerning pairs and ligature substitutions from fonts (specifically from GSUB/GPOS tables) [1]. These generated arrays are then stored within the font data structures to improve typesetting and text layout quality [1]. 3. Configuration and Customization: Developers and users can modify convert-builtin-fonts.sh to adjust which fonts are included, define point sizes, or specify additional Unicode character intervals (e.g., adding support for specialized characters or languages) [6][4][7]. Because the generated headers in lib/EpdFont/builtinFonts/ are automatically produced by this script, they should not be edited manually, as any changes will be overwritten during the build process [4]. 4. Build Pipeline: The script is used in both local development environments and automated continuous integration (CI) workflows (such as those managed by crosspoint-tools) to produce customized firmware binaries [8][9]. CrossPoint Reader also supports custom fonts loaded from an SD card, which uses a separate conversion process to create.cpfont files [10][9]. While these also utilize the project's underlying font conversion logic, they function independently of the built-in font generation performed by convert-builtin-fonts.sh [10][9].
Citations:
- 1: feat: Support for kerning and ligatures #873
- 2: https://github.com/crosspoint-reader/crosspoint-reader
- 3: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/README.md
- 4: feat: Add Latin Extended Additional (U+1E00-1E9F) to font Unicode coverage #1677
- 5: docs: add font generation commands to builtin font headers #547
- 6: Unicode Character Support: Possible Global Solution #800
- 7: feat: Arabic/Farsi/Urdu glyphs in built-in UI fonts - PR 2/3 #2596
- 8: https://github.com/crosspoint-reader/crosspoint-tools/blob/master/.github/workflows/build-custom-firmware.yml
- 9: https://github.com/crosspoint-reader/crosspoint-tools
- 10: https://github.com/alkk/crosspoint-reader
🌐 Web query:
site:github.com/crosspoint-reader/crosspoint-reader/raw/lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h "static const Epd" "Kern"
💡 Result:
I couldn’t find that exact file in the CrossPoint repo or in GitHub search results. The repo does document built-in fonts and SD-card font conversion, but lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h didn’t show up in the indexed code search. (github.com)
If you want, I can try a different path/branch guess.
Citations:
🌐 Web query:
raw.githubusercontent.com crosspoint-reader notoserif_7_bolditalic.h EpdKernClassEntry
💡 Result:
The term EpdKernClassEntry is associated with the CrossPoint Reader project, an open-source e-reader firmware for Xteink X3 and X4 devices [1][2]. While specific code snippets containing EpdKernClassEntry were not surfaced in the public repository index, it is part of the project's font rendering architecture, specifically within the lib/EpdFont subsystem, which manages e-paper display font rendering and kerning data [1]. Files such as notoserif_7_bolditalic.h are generated font header files used by the CrossPoint Reader's custom SD-card font system, created via the project's font conversion tools [1]. The EpdKernClassEntry likely represents a structure used to define kerning pairs or character spacing entries within these font data files to support proper typography on the device's e-ink display [1]. To explore this further, you can access the firmware source code on the official GitHub repository at https://github.com/crosspoint-reader/crosspoint-reader [1]. The project documentation and font builder utilities are located at https://crosspointreader.com/fonts [1].
Citations:
🌐 Web query:
crosspoint-fonts notoserif_7_bolditalic.h notosans_10_bolditalic.h
💡 Result:
The files notoserif_7_bolditalic.h and notosans_10_bolditalic.h are C header files containing font data used by the CrossPoint Reader, an open-source e-reader software for ESP32 devices [1][2]. These files represent built-in fonts, typically generated by converting standard TrueType (TTF) or OpenType (OTF) font files into a proprietary header format suitable for inclusion directly into the firmware binary [3][4][2]. Key details regarding these files include: 1. Function: They store glyph bitmap data and associated metadata (such as character dimensions and kerning information) in a C array format that the CrossPoint Reader's rendering engine can access directly from memory [3][4]. 2. Context: The project uses specific naming conventions for these header files, typically following the pattern {fontfamily}{size}{style}.h (e.g., notoserif_7_bolditalic.h is Noto Serif, 7pt, Bold-Italic style) [3][4]. 3. Generation: These files are produced using tools such as fontconvert_sdcard.py, which is part of the project's tooling [5][2]. While some fonts are included in the firmware as "built-in" [3][4], users can also generate custom fonts for SD-card use, which may result in similar file structures for use on the device [2]. 4. Repository Source: The font definitions and the scripts to generate them are managed within the project's ecosystem, specifically in repositories such as crosspoint-reader and its related tooling and font repositories [6][5][2]. If you are working with these files, they are likely located within the lib/EpdFont/builtinFonts/ directory of the firmware source code [3][4]. If you are attempting to add or modify fonts, you should use the official font conversion utilities provided in the crosspoint-tools repository rather than manually editing the header files [2].
Citations:
- 1: https://crosspointreader.com/fonts
- 2: https://github.com/crosspoint-reader/crosspoint-tools
- 3: zrn-ns@c5f8270
- 4: zrn-ns@c9b5462
- 5: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/sd-card-fonts.md
- 6: https://github.com/crosspoint-reader/crosspoint-fonts
🌐 Web query:
"notoserif_7_bolditalicKernMatrix"
💡 Result:
There is no authoritative technical record of an object, class, or variable named "notoserif_7_bolditalicKernMatrix" in the Noto Serif font project, OpenType specifications, or standard typography software libraries [1][2][3][4]. The term appears to be a synthesis of three distinct concepts related to digital typography: 1. Noto Serif: A comprehensive font family developed by Google [5][4]. 2. Bold Italic: A specific style variant within the Noto Serif family [5][6]. 3. Kern Matrix (or Kern Table): A standard component of OpenType and TrueType fonts (the 'kern' table) that defines specific spacing adjustments between pairs of glyphs to improve legibility [3][7]. While developers and typographers often work with the kerning data of the Noto Serif Bold Italic font, this data is contained within the font's internal OpenType 'kern' or 'GPOS' tables, rather than an external object with that specific name [1][3][8]. If you encountered this name in a specific programming environment or software plugin, it is likely a custom identifier or a variable name local to that specific application rather than a public, industry-standard term [9][7][10].
Citations:
- 1: https://www.mail-archive.com/ntg-context@ntg.nl/msg107689.html
- 2: Kerning and overlapping issues in heavier weights of Noto Serif notofonts/latin-greek-cyrillic#474
- 3: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html
- 4: https://notofonts.github.io/noto-docs/specimen/NotoSerif/
- 5: https://graphemica.com/fonts/notoserif-bolditalic
- 6: https://graphemica.com/fonts/notoserif-italic
- 7: https://github.com/googlefonts/ufo2ft/blob/main/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
- 8: https://www.mail-archive.com/ntg-context@ntg.nl/msg107691.html
- 9: https://tex.stackexchange.com/questions/630896/add-kern-font-feature-so-that-it-applies-to-a-combination-of-italic-and-regular
- 10: https://mirror.math.princeton.edu/pub/CTAN/macros/latex/contrib/kerntest/kerntest.pdf
🌐 Web query:
raw.githubusercontent.com crosspoint-reader notoserif_7_bolditalic.h notosans_10_bolditalic.h
💡 Result:
The files notoserif_7_bolditalic.h and notosans_10_bolditalic.h are C header files used in the CrossPoint Reader project, an open-source, community-built firmware for Xteink X3 and X4 e-reader devices [1][2][3]. These files are typically part of the firmware's font rendering system. In the context of microcontrollers like the ESP32-C3 used in these devices, font data (such as Noto Serif and Noto Sans) is often converted from standard TrueType (.ttf) files into binary arrays stored in header files (.h) to be compiled directly into the firmware image for efficient memory usage and fast access [1][4]. The CrossPoint Reader project maintains a repository at github.com/crosspoint-reader/crosspoint-reader [1][2]. The specific header files you mentioned are likely auto-generated or vendored components used by the firmware to handle UI typography [4]. Font management in CrossPoint also allows users to upload custom fonts via a web interface or copy them to an SD card for more flexibility [5]. For developers working with the source code, the project uses PlatformIO for building and flashing [6][1]. If you are looking for these files in the source tree, they would generally be located within the firmware's library or assets directories, often handled by the project's font-builder scripts [4].
Citations:
- 1: https://github.com/crosspoint-reader/crosspoint-reader
- 2: https://github.com/crosspoint-reader/
- 3: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/README.md
- 4: https://github.com/crosspoint-reader/crosspoint-tools
- 5: https://raw.githubusercontent.com/crosspoint-reader/crosspoint-reader/master/USER_GUIDE.md
- 6: https://github.com/kahnwong/crosspoint-reader
🌐 Web query:
github raw crosspoint-reader notosans_10_bolditalic.h "kern" "KernMatrix"
💡 Result:
In the CrossPoint Reader firmware project, notosans_10_bolditalic.h is a generated font header file used to define font metrics and glyph data for the Noto Sans 10pt Bold Italic style [1]. Regarding your query about "kern" and "KernMatrix": 1. KernMatrix Structure: In the CrossPoint firmware, the KernMatrix refers to a flat, 4.4 fixed-point signed integer matrix (int8_t) that stores kerning adjustments between character classes [2][3]. It is used to improve text layout accuracy by allowing fractional kerning adjustments [2][3]. 2. Implementation: The firmware's font rendering engine uses this matrix to perform lookups during text layout [4][3]. Because the kerning values and glyph advances share the same 4 fractional bits, they can be combined into a single accumulator during rendering, which is then snapped to the nearest whole pixel at render time to prevent cumulative rounding errors [2][3]. 3. Generation: These header files, including notosans_10_bolditalic.h, are auto-generated from font source files using internal scripts (typically located in lib/EpdFont/scripts/) and should not be edited by hand [1][4]. The scripts calculate these kerning matrices and other font metrics during the build process to ensure compatibility with the reader's firmware [1][5]. If you are seeing references to KernMatrix within a specific font header, it is part of the auto-generated data structure providing the necessary information for the reader's text rendering engine to handle character pair spacing [6][3].
Citations:
- 1: feat: add extra small font sizes #1043
- 2: zrn-ns@7dc5186
- 3: bcrpntr@7dc5186
- 4: refactor: Use std binary search algorithms for font lookups #1202
- 5: https://github.com/crosspoint-reader/crosspoint-tools
- 6: feat: Support for kerning and ligatures #873
Regenerate the kerning class counts from the font generator.
These built-in headers are auto-generated, so the under-counts make the last entries in notoserif_7_bolditalicKernLeftClasses/RightClasses and notosans_10_bolditalicKernLeftClasses/RightClasses excluded from the class lookup bounds. Regenerate the two fonts after fixing the generator to use the actual array sizes: 552/558 and 472/472.
📍 Affects 2 files
lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h#L3406-L3412(this comment)lib/EpdFont/builtinFonts/notosans_10_bolditalic.h#L3730-L3736
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h` around lines 3406 - 3412,
Fix the font generator to derive kerning class counts from the actual left/right
class array sizes, then regenerate both affected headers:
lib/EpdFont/builtinFonts/notoserif_7_bolditalic.h at lines 3406-3412 must use
552/558, and lib/EpdFont/builtinFonts/notosans_10_bolditalic.h at lines
3730-3736 must use 472/472, ensuring the final class entries are included in
lookup bounds.
Source: Learnings
| if (!pixels || width == 0 || height == 0) return false; | ||
| if (color == Color::Palette8 && (!palette.rgb || palette.count == 0 || palette.count > 256)) return false; | ||
|
|
||
| const uint32_t bytesPerPixel = (color == Color::Rgb8) ? 3 : 1; | ||
| const uint64_t rowBytes = (uint64_t)width * bytesPerPixel; | ||
| // Raw zlib payload: one filter byte (0 = None) per scanline, then the row. | ||
| const uint64_t rawLen = (rowBytes + 1) * height; | ||
| if (rawLen > 32u * 1024 * 1024) { | ||
| LOG_ERR("PNG", "image too large: %ux%u", width, height); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
encode cannot actually report allocation failure.
idat.reserve()/out.insert() abort under -fno-exceptions instead of returning false, so the header's "Returns false when ... allocation fails" contract only holds because callers pre-probe (see MobiToEpub.cpp ensureImage). Either probe the worst case here with makeUniqueNoThrow (as GifDecoder.cpp does at lines 332-338) or tighten the header doc to say callers must pre-check available memory.
Also applies to: 78-79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/ImageTranscode/PngWriter.cpp` around lines 33 - 43, The encode
implementation does not honor its documented allocation-failure behavior because
idat.reserve() and out.insert() can abort without exceptions. Update encode to
perform a worst-case non-throwing allocation probe using makeUniqueNoThrow,
following the GifDecoder.cpp pattern, and return false when the probe fails
before proceeding with buffer allocations.
| uint32_t cp = 0; | ||
| if (c == '&') { | ||
| // Entity | ||
| size_t j = i + 1; | ||
| if (j < len && html[j] == '#') { | ||
| j++; | ||
| uint32_t v = 0; | ||
| if (j < len && (html[j] == 'x' || html[j] == 'X')) { | ||
| j++; | ||
| while (j < len && isxdigit((unsigned char)html[j])) { | ||
| v = v * 16 + (isdigit((unsigned char)html[j]) ? html[j] - '0' : (tolower((unsigned char)html[j]) - 'a' + 10)); | ||
| j++; | ||
| } | ||
| } else { | ||
| while (j < len && isdigit((unsigned char)html[j])) v = v * 10 + (html[j++] - '0'); | ||
| } | ||
| if (j < len && html[j] == ';') { | ||
| appendTextChar(b.chapterBuf, v ? v : 0xFFFD); | ||
| i = j + 1; | ||
| } else { | ||
| b.chapterBuf += "&"; | ||
| i++; | ||
| } | ||
| if (b.inHeading && b.pendingHeadingText.size() < 64) b.pendingHeadingText += '?'; | ||
| continue; | ||
| } | ||
| size_t nameLen = 0; | ||
| while (j + nameLen < len && nameLen < 12 && isalnum((unsigned char)html[j + nameLen])) nameLen++; | ||
| if (j + nameLen < len && html[j + nameLen] == ';' && nameLen > 0) { | ||
| const int ecp = lookupEntity(html + j, nameLen); | ||
| if (ecp >= 0) { | ||
| appendTextChar(b.chapterBuf, (uint32_t)ecp); | ||
| if (b.inHeading && b.pendingHeadingText.size() < 64 && ecp < 128 && ecp >= 32) | ||
| b.pendingHeadingText += (char)ecp; | ||
| } else { | ||
| b.chapterBuf += "&"; | ||
| b.chapterBuf.append(html + j, nameLen + 1); | ||
| } | ||
| i = j + nameLen + 1; | ||
| continue; | ||
| } | ||
| b.chapterBuf += "&"; | ||
| i++; | ||
| continue; | ||
| } | ||
|
|
||
| if (b.chapterBuf.size() > CHAPTER_HARD_LIMIT) b.forceSplit(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
The chapter hard limit is bypassed by the entity path.
Both &-branches continue before reaching the check at Line 648, and the tag-side checks (Lines 547-551) only fire when a whitelisted tag is seen. A body that is a long run of character references ( , —, …) with no whitelisted block tags therefore grows chapterBuf past CHAPTER_HARD_LIMIT without ever splitting — up to several MB for the permitted 24MB input — which is the OOM the hard limit was added to prevent. Move the check above the text handling so every text byte is covered.
🛡️ Proposed fix
// Text content
+ if (b.chapterBuf.size() > CHAPTER_HARD_LIMIT) b.forceSplit();
uint32_t cp = 0;
if (c == '&') {
@@
- if (b.chapterBuf.size() > CHAPTER_HARD_LIMIT) b.forceSplit();
-
const uint8_t uc = (uint8_t)c;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uint32_t cp = 0; | |
| if (c == '&') { | |
| // Entity | |
| size_t j = i + 1; | |
| if (j < len && html[j] == '#') { | |
| j++; | |
| uint32_t v = 0; | |
| if (j < len && (html[j] == 'x' || html[j] == 'X')) { | |
| j++; | |
| while (j < len && isxdigit((unsigned char)html[j])) { | |
| v = v * 16 + (isdigit((unsigned char)html[j]) ? html[j] - '0' : (tolower((unsigned char)html[j]) - 'a' + 10)); | |
| j++; | |
| } | |
| } else { | |
| while (j < len && isdigit((unsigned char)html[j])) v = v * 10 + (html[j++] - '0'); | |
| } | |
| if (j < len && html[j] == ';') { | |
| appendTextChar(b.chapterBuf, v ? v : 0xFFFD); | |
| i = j + 1; | |
| } else { | |
| b.chapterBuf += "&"; | |
| i++; | |
| } | |
| if (b.inHeading && b.pendingHeadingText.size() < 64) b.pendingHeadingText += '?'; | |
| continue; | |
| } | |
| size_t nameLen = 0; | |
| while (j + nameLen < len && nameLen < 12 && isalnum((unsigned char)html[j + nameLen])) nameLen++; | |
| if (j + nameLen < len && html[j + nameLen] == ';' && nameLen > 0) { | |
| const int ecp = lookupEntity(html + j, nameLen); | |
| if (ecp >= 0) { | |
| appendTextChar(b.chapterBuf, (uint32_t)ecp); | |
| if (b.inHeading && b.pendingHeadingText.size() < 64 && ecp < 128 && ecp >= 32) | |
| b.pendingHeadingText += (char)ecp; | |
| } else { | |
| b.chapterBuf += "&"; | |
| b.chapterBuf.append(html + j, nameLen + 1); | |
| } | |
| i = j + nameLen + 1; | |
| continue; | |
| } | |
| b.chapterBuf += "&"; | |
| i++; | |
| continue; | |
| } | |
| if (b.chapterBuf.size() > CHAPTER_HARD_LIMIT) b.forceSplit(); | |
| if (b.chapterBuf.size() > CHAPTER_HARD_LIMIT) b.forceSplit(); | |
| uint32_t cp = 0; | |
| if (c == '&') { | |
| // Entity | |
| size_t j = i + 1; | |
| if (j < len && html[j] == '#') { | |
| j++; | |
| uint32_t v = 0; | |
| if (j < len && (html[j] == 'x' || html[j] == 'X')) { | |
| j++; | |
| while (j < len && isxdigit((unsigned char)html[j])) { | |
| v = v * 16 + (isdigit((unsigned char)html[j]) ? html[j] - '0' : (tolower((unsigned char)html[j]) - 'a' + 10)); | |
| j++; | |
| } | |
| } else { | |
| while (j < len && isdigit((unsigned char)html[j])) v = v * 10 + (html[j++] - '0'); | |
| } | |
| if (j < len && html[j] == ';') { | |
| appendTextChar(b.chapterBuf, v ? v : 0xFFFD); | |
| i = j + 1; | |
| } else { | |
| b.chapterBuf += "&"; | |
| i++; | |
| } | |
| if (b.inHeading && b.pendingHeadingText.size() < 64) b.pendingHeadingText += '?'; | |
| continue; | |
| } | |
| size_t nameLen = 0; | |
| while (j + nameLen < len && nameLen < 12 && isalnum((unsigned char)html[j + nameLen])) nameLen++; | |
| if (j + nameLen < len && html[j + nameLen] == ';' && nameLen > 0) { | |
| const int ecp = lookupEntity(html + j, nameLen); | |
| if (ecp >= 0) { | |
| appendTextChar(b.chapterBuf, (uint32_t)ecp); | |
| if (b.inHeading && b.pendingHeadingText.size() < 64 && ecp < 128 && ecp >= 32) | |
| b.pendingHeadingText += (char)ecp; | |
| } else { | |
| b.chapterBuf += "&"; | |
| b.chapterBuf.append(html + j, nameLen + 1); | |
| } | |
| i = j + nameLen + 1; | |
| continue; | |
| } | |
| b.chapterBuf += "&"; | |
| i++; | |
| continue; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/Mobi/MobiToEpub.cpp` around lines 602 - 648, Move the CHAPTER_HARD_LIMIT
check in the MobiToEpub text-processing flow so it executes before the entity
branches in the & handling logic, ensuring every appended text byte—including
decoded character references—can trigger b.forceSplit(). Preserve the existing
entity decoding and heading-text behavior while removing the bypass caused by
early continue statements.
| void PdfDoc::parseTrailerDict(const PdfObj& tr, uint32_t& prevOut, uint32_t& xrefStmOut) { | ||
| if (const PdfObj* r = tr.find("Root"); r && r->kind == Kind::Ref && rootNum == 0) rootNum = r->ref; | ||
| if (const PdfObj* i = tr.find("Info"); i && i->kind == Kind::Ref && infoNum == 0) infoNum = i->ref; | ||
| if (const PdfObj* e = tr.find("Encrypt"); e && e->kind != Kind::Null) encrypted = true; | ||
| if (const PdfObj* p = tr.find("Prev"); p && p->isNum()) prevOut = (uint32_t)p->num; | ||
| if (const PdfObj* x = tr.find("XRefStm"); x && x->isNum()) xrefStmOut = (uint32_t)x->num; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clamp double → uint32_t conversions.
PdfObj::num comes from strtod, so a file containing /Prev 1e300 (or /Size, /XRefStm, /Index, /Length) makes these casts undefined behavior rather than a large-but-harmless value — the PR's UBSan harness will trip on it. Same pattern at Lines 309, 321-322 and 466/469.
🛡️ Suggested helper
+uint32_t u32Of(const PdfObj& o) { // in the anonymous namespace
+ return (o.num >= 0.0 && o.num < 4294967296.0) ? (uint32_t)o.num : 0;
+}- if (const PdfObj* p = tr.find("Prev"); p && p->isNum()) prevOut = (uint32_t)p->num;
- if (const PdfObj* x = tr.find("XRefStm"); x && x->isNum()) xrefStmOut = (uint32_t)x->num;
+ if (const PdfObj* p = tr.find("Prev"); p && p->isNum()) prevOut = u32Of(*p);
+ if (const PdfObj* x = tr.find("XRefStm"); x && x->isNum()) xrefStmOut = u32Of(*x);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void PdfDoc::parseTrailerDict(const PdfObj& tr, uint32_t& prevOut, uint32_t& xrefStmOut) { | |
| if (const PdfObj* r = tr.find("Root"); r && r->kind == Kind::Ref && rootNum == 0) rootNum = r->ref; | |
| if (const PdfObj* i = tr.find("Info"); i && i->kind == Kind::Ref && infoNum == 0) infoNum = i->ref; | |
| if (const PdfObj* e = tr.find("Encrypt"); e && e->kind != Kind::Null) encrypted = true; | |
| if (const PdfObj* p = tr.find("Prev"); p && p->isNum()) prevOut = (uint32_t)p->num; | |
| if (const PdfObj* x = tr.find("XRefStm"); x && x->isNum()) xrefStmOut = (uint32_t)x->num; | |
| } | |
| void PdfDoc::parseTrailerDict(const PdfObj& tr, uint32_t& prevOut, uint32_t& xrefStmOut) { | |
| if (const PdfObj* r = tr.find("Root"); r && r->kind == Kind::Ref && rootNum == 0) rootNum = r->ref; | |
| if (const PdfObj* i = tr.find("Info"); i && i->kind == Kind::Ref && infoNum == 0) infoNum = i->ref; | |
| if (const PdfObj* e = tr.find("Encrypt"); e && e->kind != Kind::Null) encrypted = true; | |
| if (const PdfObj* p = tr.find("Prev"); p && p->isNum()) prevOut = u32Of(*p); | |
| if (const PdfObj* x = tr.find("XRefStm"); x && x->isNum()) xrefStmOut = u32Of(*x); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/Pdf/PdfDoc.cpp` around lines 356 - 362, Clamp every conversion from
PdfObj::num (double) to uint32_t in parseTrailerDict and the other identified
parsing sites around the /Size, /XRefStm, /Index, and /Length handling to the
uint32_t range before casting. Reuse a shared finite-and-bounded conversion
helper so values such as 1e300 cannot trigger undefined behavior under UBSan,
while preserving normal in-range values.
| if (strcmp(op, "begincodespacerange") == 0) { | ||
| for (int i = 0; i < 256; i++) { | ||
| lx.skipWs(); | ||
| if (lx.atEnd() || lx.keyword("endcodespacerange")) break; | ||
| PdfObj lo, hi; | ||
| if (!lx.parseObject(lo) || !lx.parseObject(hi)) break; | ||
| if (lo.kind == Kind::String && !lo.str.empty()) { | ||
| codeBytes = lo.str.size() >= 2 ? 2 : 1; | ||
| sawCodeSpace = true; | ||
| } | ||
| } | ||
| } else if (strcmp(op, "beginbfchar") == 0) { | ||
| for (size_t i = 0; i < MAX_CMAP_ENTRIES; i++) { | ||
| lx.skipWs(); | ||
| if (lx.atEnd() || lx.keyword("endbfchar")) break; | ||
| PdfObj src, dst; | ||
| if (!lx.parseObject(src) || !lx.parseObject(dst)) break; | ||
| if (src.kind == Kind::String && dst.kind == Kind::String) { | ||
| const uint32_t code = codeOf(src.str); | ||
| addEntry(code, code, dst.str); | ||
| if (!sawCodeSpace && src.str.size() >= 2) codeBytes = 2; | ||
| } | ||
| } | ||
| } else if (strcmp(op, "beginbfrange") == 0) { | ||
| for (size_t i = 0; i < MAX_CMAP_ENTRIES; i++) { | ||
| lx.skipWs(); | ||
| if (lx.atEnd() || lx.keyword("endbfrange")) break; | ||
| PdfObj lo, hi, dst; | ||
| if (!lx.parseObject(lo) || !lx.parseObject(hi) || !lx.parseObject(dst)) break; | ||
| if (lo.kind != Kind::String || hi.kind != Kind::String) break; | ||
| const uint32_t cLo = codeOf(lo.str); | ||
| const uint32_t cHi = codeOf(hi.str); | ||
| if (!sawCodeSpace && lo.str.size() >= 2) codeBytes = 2; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Only widen codeBytes for Type0 fonts.
Simple fonts are single-byte encoded (ISO 32000-1 §9.6), but a ToUnicode CMap that writes codes as 2-byte hex strings (<0041>, and generators do emit these) sets codeBytes = 2 here. decode() then steps two bytes per glyph for a simple font and silently loses roughly half the page text.
🐛 Proposed fix
if (lo.kind == Kind::String && !lo.str.empty()) {
- codeBytes = lo.str.size() >= 2 ? 2 : 1;
+ if (isCID) codeBytes = lo.str.size() >= 2 ? 2 : 1;
sawCodeSpace = true;
}
@@
- if (!sawCodeSpace && src.str.size() >= 2) codeBytes = 2;
+ if (isCID && !sawCodeSpace && src.str.size() >= 2) codeBytes = 2;
@@
- if (!sawCodeSpace && lo.str.size() >= 2) codeBytes = 2;
+ if (isCID && !sawCodeSpace && lo.str.size() >= 2) codeBytes = 2;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (strcmp(op, "begincodespacerange") == 0) { | |
| for (int i = 0; i < 256; i++) { | |
| lx.skipWs(); | |
| if (lx.atEnd() || lx.keyword("endcodespacerange")) break; | |
| PdfObj lo, hi; | |
| if (!lx.parseObject(lo) || !lx.parseObject(hi)) break; | |
| if (lo.kind == Kind::String && !lo.str.empty()) { | |
| codeBytes = lo.str.size() >= 2 ? 2 : 1; | |
| sawCodeSpace = true; | |
| } | |
| } | |
| } else if (strcmp(op, "beginbfchar") == 0) { | |
| for (size_t i = 0; i < MAX_CMAP_ENTRIES; i++) { | |
| lx.skipWs(); | |
| if (lx.atEnd() || lx.keyword("endbfchar")) break; | |
| PdfObj src, dst; | |
| if (!lx.parseObject(src) || !lx.parseObject(dst)) break; | |
| if (src.kind == Kind::String && dst.kind == Kind::String) { | |
| const uint32_t code = codeOf(src.str); | |
| addEntry(code, code, dst.str); | |
| if (!sawCodeSpace && src.str.size() >= 2) codeBytes = 2; | |
| } | |
| } | |
| } else if (strcmp(op, "beginbfrange") == 0) { | |
| for (size_t i = 0; i < MAX_CMAP_ENTRIES; i++) { | |
| lx.skipWs(); | |
| if (lx.atEnd() || lx.keyword("endbfrange")) break; | |
| PdfObj lo, hi, dst; | |
| if (!lx.parseObject(lo) || !lx.parseObject(hi) || !lx.parseObject(dst)) break; | |
| if (lo.kind != Kind::String || hi.kind != Kind::String) break; | |
| const uint32_t cLo = codeOf(lo.str); | |
| const uint32_t cHi = codeOf(hi.str); | |
| if (!sawCodeSpace && lo.str.size() >= 2) codeBytes = 2; | |
| if (strcmp(op, "begincodespacerange") == 0) { | |
| for (int i = 0; i < 256; i++) { | |
| lx.skipWs(); | |
| if (lx.atEnd() || lx.keyword("endcodespacerange")) break; | |
| PdfObj lo, hi; | |
| if (!lx.parseObject(lo) || !lx.parseObject(hi)) break; | |
| if (lo.kind == Kind::String && !lo.str.empty()) { | |
| if (isCID) codeBytes = lo.str.size() >= 2 ? 2 : 1; | |
| sawCodeSpace = true; | |
| } | |
| } | |
| } else if (strcmp(op, "beginbfchar") == 0) { | |
| for (size_t i = 0; i < MAX_CMAP_ENTRIES; i++) { | |
| lx.skipWs(); | |
| if (lx.atEnd() || lx.keyword("endbfchar")) break; | |
| PdfObj src, dst; | |
| if (!lx.parseObject(src) || !lx.parseObject(dst)) break; | |
| if (src.kind == Kind::String && dst.kind == Kind::String) { | |
| const uint32_t code = codeOf(src.str); | |
| addEntry(code, code, dst.str); | |
| if (isCID && !sawCodeSpace && src.str.size() >= 2) codeBytes = 2; | |
| } | |
| } | |
| } else if (strcmp(op, "beginbfrange") == 0) { | |
| for (size_t i = 0; i < MAX_CMAP_ENTRIES; i++) { | |
| lx.skipWs(); | |
| if (lx.atEnd() || lx.keyword("endbfrange")) break; | |
| PdfObj lo, hi, dst; | |
| if (!lx.parseObject(lo) || !lx.parseObject(hi) || !lx.parseObject(dst)) break; | |
| if (lo.kind != Kind::String || hi.kind != Kind::String) break; | |
| const uint32_t cLo = codeOf(lo.str); | |
| const uint32_t cHi = codeOf(hi.str); | |
| if (!sawCodeSpace && lo.str.size() >= 2) codeBytes = 2; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/Pdf/PdfFont.cpp` around lines 361 - 393, Restrict the codeBytes widening
logic in the CMap parsing branches around begincodespacerange, beginbfchar, and
beginbfrange to Type0 fonts only. Preserve single-byte decoding for simple fonts
even when the ToUnicode CMap contains two-byte hex codes, while retaining the
existing two-byte behavior for Type0 fonts.
| bool ZipWriter::begin(const char* path) { | ||
| out = std::make_unique<HalFile>(); | ||
| if (!Storage.openFileForWrite("ZIPW", path, *out)) { | ||
| LOG_ERR("ZIPW", "open for write failed: %s", path); | ||
| out.reset(); | ||
| return false; | ||
| } | ||
| offset = 0; | ||
| entries.clear(); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
begin() doesn't reset streaming.
If a conversion is abandoned between beginStreamedFile() and endStreamedFile() without calling abort(), streaming stays true; a subsequent begin() starts a fresh archive in which every addFile/beginStreamedFile/finish returns false. Reset the streaming state along with offset and entries.
🔧 Proposed fix
offset = 0;
+ streaming = false;
+ streamWritten = 0;
+ streamDeclared = 0;
entries.clear();
return true;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| bool ZipWriter::begin(const char* path) { | |
| out = std::make_unique<HalFile>(); | |
| if (!Storage.openFileForWrite("ZIPW", path, *out)) { | |
| LOG_ERR("ZIPW", "open for write failed: %s", path); | |
| out.reset(); | |
| return false; | |
| } | |
| offset = 0; | |
| entries.clear(); | |
| return true; | |
| } | |
| bool ZipWriter::begin(const char* path) { | |
| out = std::make_unique<HalFile>(); | |
| if (!Storage.openFileForWrite("ZIPW", path, *out)) { | |
| LOG_ERR("ZIPW", "open for write failed: %s", path); | |
| out.reset(); | |
| return false; | |
| } | |
| offset = 0; | |
| streaming = false; | |
| streamWritten = 0; | |
| streamDeclared = 0; | |
| entries.clear(); | |
| return true; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/ZipWriter/ZipWriter.cpp` around lines 30 - 40, Update ZipWriter::begin to
reset the streaming state to false alongside offset and entries when starting a
new archive, ensuring subsequent addFile, beginStreamedFile, and finish
operations work after an abandoned streamed conversion.
| void AudioPlayerActivity::buildPlaylist() { | ||
| playlist.clear(); | ||
| const std::string folder = FsHelpers::extractFolderPath(initialPath); | ||
| auto dir = Storage.open(folder.c_str()); | ||
| if (dir && dir.isDirectory()) { | ||
| char nameBuf[256]; | ||
| while (playlist.size() < PLAYLIST_MAX) { | ||
| auto entry = dir.openNextFile(); | ||
| if (!entry) break; | ||
| if (entry.isDirectory()) continue; | ||
| entry.getName(nameBuf, sizeof(nameBuf)); | ||
| if (nameBuf[0] == '.') continue; | ||
| if (!FsHelpers::hasAudioExtension(std::string_view(nameBuf))) continue; | ||
| playlist.push_back(folder == "/" ? "/" + std::string(nameBuf) : folder + "/" + std::string(nameBuf)); | ||
| } | ||
| } | ||
| FsHelpers::sortFileList(playlist); | ||
| trackIndex = 0; | ||
| for (size_t i = 0; i < playlist.size(); i++) { | ||
| if (playlist[i] == initialPath) { | ||
| trackIndex = i; | ||
| break; | ||
| } | ||
| } | ||
| if (playlist.empty()) playlist.push_back(initialPath); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
initialPath can be silently dropped when the folder has more than PLAYLIST_MAX audio files.
The scan loop breaks once playlist.size() == PLAYLIST_MAX, in raw filesystem enumeration order (before sorting). The later "is initialPath present?" search only falls back to push_back when the playlist is empty — if the tapped file simply isn't among the first 500 entries encountered, trackIndex silently stays 0 and a different track plays instead of the one the user opened.
🐛 Proposed fix to guarantee the tapped file is always selected
FsHelpers::sortFileList(playlist);
trackIndex = 0;
- for (size_t i = 0; i < playlist.size(); i++) {
- if (playlist[i] == initialPath) {
- trackIndex = i;
- break;
- }
- }
- if (playlist.empty()) playlist.push_back(initialPath);
+ bool found = false;
+ for (size_t i = 0; i < playlist.size(); i++) {
+ if (playlist[i] == initialPath) {
+ trackIndex = i;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ playlist.push_back(initialPath);
+ trackIndex = playlist.size() - 1;
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void AudioPlayerActivity::buildPlaylist() { | |
| playlist.clear(); | |
| const std::string folder = FsHelpers::extractFolderPath(initialPath); | |
| auto dir = Storage.open(folder.c_str()); | |
| if (dir && dir.isDirectory()) { | |
| char nameBuf[256]; | |
| while (playlist.size() < PLAYLIST_MAX) { | |
| auto entry = dir.openNextFile(); | |
| if (!entry) break; | |
| if (entry.isDirectory()) continue; | |
| entry.getName(nameBuf, sizeof(nameBuf)); | |
| if (nameBuf[0] == '.') continue; | |
| if (!FsHelpers::hasAudioExtension(std::string_view(nameBuf))) continue; | |
| playlist.push_back(folder == "/" ? "/" + std::string(nameBuf) : folder + "/" + std::string(nameBuf)); | |
| } | |
| } | |
| FsHelpers::sortFileList(playlist); | |
| trackIndex = 0; | |
| for (size_t i = 0; i < playlist.size(); i++) { | |
| if (playlist[i] == initialPath) { | |
| trackIndex = i; | |
| break; | |
| } | |
| } | |
| if (playlist.empty()) playlist.push_back(initialPath); | |
| } | |
| void AudioPlayerActivity::buildPlaylist() { | |
| playlist.clear(); | |
| const std::string folder = FsHelpers::extractFolderPath(initialPath); | |
| auto dir = Storage.open(folder.c_str()); | |
| if (dir && dir.isDirectory()) { | |
| char nameBuf[256]; | |
| while (playlist.size() < PLAYLIST_MAX) { | |
| auto entry = dir.openNextFile(); | |
| if (!entry) break; | |
| if (entry.isDirectory()) continue; | |
| entry.getName(nameBuf, sizeof(nameBuf)); | |
| if (nameBuf[0] == '.') continue; | |
| if (!FsHelpers::hasAudioExtension(std::string_view(nameBuf))) continue; | |
| playlist.push_back(folder == "/" ? "/" + std::string(nameBuf) : folder + "/" + std::string(nameBuf)); | |
| } | |
| } | |
| FsHelpers::sortFileList(playlist); | |
| trackIndex = 0; | |
| bool found = false; | |
| for (size_t i = 0; i < playlist.size(); i++) { | |
| if (playlist[i] == initialPath) { | |
| trackIndex = i; | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| playlist.push_back(initialPath); | |
| trackIndex = playlist.size() - 1; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/activities/audio/AudioPlayerActivity.cpp` around lines 148 - 173, Update
AudioPlayerActivity::buildPlaylist so initialPath is always included and
selected even when the directory contains more than PLAYLIST_MAX audio files.
Preserve the playlist cap for other entries, but reserve space for or explicitly
retain initialPath when it is encountered or absent after scanning, then sort
and assign trackIndex to its matching entry; do not rely on the existing
empty-playlist fallback.
| const int rowsOnPage = std::min(pageItems, std::max(0, settingsCount - pageStart)); | ||
| if (rowsOnPage <= 0) return false; | ||
| if (row >= rowsOnPage) { | ||
| if (row > rowsOnPage) return false; | ||
| row = rowsOnPage - 1; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clamp all trailing rows on a partial page.
When rowsOnPage < pageItems, row > rowsOnPage still rejects lower blank rows. Clamp every row >= rowsOnPage to rowsOnPage - 1 so the entire trailing area selects the last setting. Based on learnings, this is a Minor UX correctness issue rather than a Major defect.
Proposed fix
- if (row >= rowsOnPage) {
- if (row > rowsOnPage) return false;
- row = rowsOnPage - 1;
- }
+ if (row >= rowsOnPage) row = rowsOnPage - 1;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const int rowsOnPage = std::min(pageItems, std::max(0, settingsCount - pageStart)); | |
| if (rowsOnPage <= 0) return false; | |
| if (row >= rowsOnPage) { | |
| if (row > rowsOnPage) return false; | |
| row = rowsOnPage - 1; | |
| } | |
| const int rowsOnPage = std::min(pageItems, std::max(0, settingsCount - pageStart)); | |
| if (rowsOnPage <= 0) return false; | |
| if (row >= rowsOnPage) row = rowsOnPage - 1; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/activities/settings/SettingsActivity.cpp` around lines 207 - 212, Update
the row-clamping logic in the settings pagination flow so every row greater than
or equal to rowsOnPage is clamped to rowsOnPage - 1, including rows beyond the
final populated row on a partial page. Preserve the existing rowsOnPage <= 0
early return and normal in-range row behavior.
Source: Learnings
| #if FREEINK_DEVICE_MURPHY | ||
| // 416x240 panel: 14pt reads huge there, 8pt is the comfortable default. | ||
| static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 7; | ||
| #else | ||
| static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 14; | ||
| #endif |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Comment says 8pt, constant is 7.
The 8pt figure is the pre-murphyReaderDefaultsV2 default (see the marker comment at Lines 274-277). Align the text with the value.
📝 Proposed comment fix
`#if` FREEINK_DEVICE_MURPHY
- // 416x240 panel: 14pt reads huge there, 8pt is the comfortable default.
+ // 416x240 panel: 14pt reads huge there, 7pt is the comfortable default.
static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 7;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #if FREEINK_DEVICE_MURPHY | |
| // 416x240 panel: 14pt reads huge there, 8pt is the comfortable default. | |
| static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 7; | |
| #else | |
| static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 14; | |
| #endif | |
| `#if` FREEINK_DEVICE_MURPHY | |
| // 416x240 panel: 14pt reads huge there, 7pt is the comfortable default. | |
| static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 7; | |
| `#else` | |
| static constexpr uint8_t DEFAULT_FONT_POINT_SIZE = 14; | |
| `#endif` |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/CrossPointSettings.h` around lines 101 - 106, Update the Murphy-specific
comment above DEFAULT_FONT_POINT_SIZE to state that 7pt is the comfortable
default, matching the constant and the murphyReaderDefaultsV2 value. Leave the
non-Murphy branch unchanged.
Source: Learnings
| // Frontlight brightness percent (0 = off, max 100, steps of 10). Applied at | ||
| // boot and on change via FrontlightManager on boards that wire a frontlight | ||
| // (e.g. Murphy). Persisted manually in toJson/fromJson — the SettingsList | ||
| // entry is dynamic, so the generic loop skips it. | ||
| uint8_t frontlightBrightness = 0; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Doc says steps of 10; the UI exposes steps of 20.
buildFrontlightSetting() in src/SettingsList.h uses FRONTLIGHT_UI_STEP_PERCENT = 20 with values 0/20/40/60/80/100.
📝 Proposed comment fix
- // Frontlight brightness percent (0 = off, max 100, steps of 10). Applied at
+ // Frontlight brightness percent (0 = off, max 100, UI steps of 20). Applied at📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Frontlight brightness percent (0 = off, max 100, steps of 10). Applied at | |
| // boot and on change via FrontlightManager on boards that wire a frontlight | |
| // (e.g. Murphy). Persisted manually in toJson/fromJson — the SettingsList | |
| // entry is dynamic, so the generic loop skips it. | |
| uint8_t frontlightBrightness = 0; | |
| // Frontlight brightness percent (0 = off, max 100, UI steps of 20). Applied at | |
| // boot and on change via FrontlightManager on boards that wire a frontlight | |
| // (e.g. Murphy). Persisted manually in toJson/fromJson — the SettingsList | |
| // entry is dynamic, so the generic loop skips it. | |
| uint8_t frontlightBrightness = 0; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/CrossPointSettings.h` around lines 266 - 270, Update the documentation
for CrossPointSettings::frontlightBrightness to state that brightness values use
UI steps of 20, matching FRONTLIGHT_UI_STEP_PERCENT and buildFrontlightSetting()
while retaining the existing 0–100 percent range.
Murphy M3 support, small-panel UI scaling, and native MOBI / PDF / audio
This brings CrossPoint up on a second class of hardware — the Murphy M3
(HamGeek M3 / corogoo 墨菲), an ESP32-S3R8 e-reader with a 3.7" 240×416 UC8253
panel, capacitive touch, a frontlight, an ES8388 audio codec and 8 MB of PSRAM —
and adds the format and feature work that device makes possible.
The hardware-level work (SD, touch, waveforms, battery, charging) is a companion
PR against
freeink-sdk. This PR is the application side.It is deliberately reviewable in pieces. Several parts stand alone and are
useful on existing devices; the three large features at the end are independent
of each other and of the port. Take any subset — I'd rather land the useful
half than argue about the whole.
Part 1 — Small-panel support (useful beyond this device)
UI density scaling
The theme metric sets were hand-tuned for the 480×800-logical X4/X3 class. The
M3's logical canvas is 240×416 at ~130 PPI, so the same pixel values render
physically larger and out of proportion to a canvas with a quarter of the area.
Buttons overflowed the panel and their labels were unreadable.
uiScaleexisted in settings but was dead code. Rather than revive a runtimescale (which would multiply every layout calculation at render time), each
theme's
constexprmetric set is passed throughscaledMetrics()at compiletime. Touch-relevant heights run through a floor so scaling can never produce a
sub-finger target (~28 px ≈ 5.5 mm at this PPI). One knob,
kUiDensityScale,currently 0.6 for this device and 1.0 everywhere else — so no existing device
changes by a single pixel.
The classic theme's home menu also now refuses to paint a row the panel cannot
show, instead of drawing half-tiles off the bottom edge, and caps the cover tile
on small panels so the full menu fits.
A list hit-test bug worth having on every device
A list band is rarely an exact multiple of the row height, so the leftover pixels
below the last row belonged to no row, and taps there were silently discarded.
Whether a given screen had that dead strip depended on its band height happening
to divide evenly by the row height — which is why touch appeared to work in some
menus and not in others. It is worst on a short panel (28 px rows in a 354 px
band leaves an 18 px strip, over half a row), but the same dead zone exists on
the larger devices.
The last drawn row now extends to the bottom of the band, both in the shared
MappedInputManagerhit-test and inSettingsActivity's own copy of it. Tapsgenuinely below the list content are still ignored.
Reader font sizes down to 5 pt
Murphy's reader sizes are 5/6/7/8/9/10 pt (default 7), with 12–18 pt compiled
out on that device to keep the image within the app partition. 32 new cuts
(NotoSerif and NotoSans, four styles each, 5–8 pt) plus 7/8 pt UI cuts.
A quantization fix that matters on any 1-bit panel
fontconvert.pyreduces 4-bit coverage to 2 bits and treats coverage 1–3 aswhite. On a grayscale panel that is invisible either way. On a 1-bit panel —
where every ink level is painted black — those pixels are not "faint grey",
they are missing ink: I measured ~20 % of all stroke ink in a 7 pt cut being
discarded, which reads as patchy, uneven, washed-out text with visible gaps in
thin strokes.
New
--ink-flooroption (default 4, so every existing cut is byte-identical)sets that cutoff. Murphy's small cuts are generated with
--ink-floor 3, whichwas picked by measurement and then confirmed on the panel: of the stroke ink in a
7 pt glyph the default cut paints 79 %, floor 3 paints 85 %, and floor 2 paints
90 % and reads too thick. Glyph metrics are untouched, so layout and existing
caches stay valid.
Grayscale capability gating
This panel has no usable grayscale (asymmetric VSH/VSL rails — see the SDK PR).
PanelDriver::supportsGrayscale()from the SDK is plumbed throughHalDisplay→GfxRenderer, and the four call sites that build or display grayplanes are gated on it:
EpubReaderActivity's text/any-grayscale decisions,ReaderUtils::renderAntiAliased,XtcReaderActivity's gray passes, andSleepActivity. Without this, thedisplayGraydefault re-displays the rawgray plane as a B/W frame and every EPUB page renders near-solid black. It is
also a page-turn speed win on such panels, since the second render is skipped.
Frontlight
No CrossPoint device had one, so there was no support at all. Added as
Settings → Display → Frontlight (0–100 % in steps), applied live, persisted,
restored at boot, exposed through the web settings API, and guarded with
__has_includeso builds without the SDK'sFrontlightManagerare unaffected.Part 2 — Native MOBI / AZW support
CrossPoint can now open
.mobi,.azwand the MOBI6 part of.azw3directly.Approach. The reader's pipeline boundary is a well-formed XHTML file on the
SD card —
Sectioninflates a spine entry to a cache file andexpatparsesit. So rather than build a parallel reader, a MOBI is converted to an EPUB
once, on device, into
/.crosspoint/mobi_<hash>/book.epub; from then on theexisting stack handles it with TOC, pagination cache, progress, bookmarks, font
choices and everything else working unchanged.
lib/Mobicontains:MobiParser— PDB container walk, MOBI and EXTH headers, and text-recorddecompression for all three schemes: none, PalmDOC LZ77, and HUFF/CDIC
(Huffman-compressed books, including the multi-dictionary phrase expansion).
Handles the trailing-entry stripping described by
extraDataFlags. DRM'd booksand pure-KF8
.azw3files are rejected with a message the UI can show, ratherthan producing garbage.
MobiToEpub— the markup tidier. MOBI is HTML-3.2-ish and routinely notXML-well-formed, while
expatdownstream is strict, so this is where the realwork is: a tag whitelist with automatic closing of dangling blocks, void
elements self-closed, named entities mapped to numeric ones, cp1252 → UTF-8
transcoding for books that declare it,
<mbp:pagebreak>honoured as a chaptersplit with a size-based fallback,
recindeximages extracted, and a TOC builtfrom each chapter's first heading. Unknown tags are stripped but their text is
always kept — no book loses content to an unrecognised element.
lib/ZipWriter— a small stored-entry ZIP writer (EPUB needs a container;deflate buys nothing on an SD card and stored entries keep the reader's
inflate path untouched).
lib/ImageTranscode— a PNG encoder, plus GIF decoding. MOBI booksfrequently store illustrations as GIF, which the reader cannot decode, so GIFs
are transcoded to PNG during conversion. The encoder emits stored deflate
blocks because the vendored miniz has its compressor compiled out; the output
is a valid PNG that costs SD space rather than CPU, paid once per image at
conversion time.
Conversions carry a converter version, so a firmware upgrade regenerates them
rather than silently reusing output from an older build.
Part 3 — Native PDF support (text reflow)
.pdffiles open as reflowable books, converted the same way into/.crosspoint/pdf_<hash>/book.epub.lib/Pdfimplements enough of ISO 32000-1 to get text and images out:classic cross-reference tables and 1.5+ cross-reference streams (with PNG and
TIFF predictors), object streams,
FlateDecode/ASCIIHexDecode/RunLengthDecode,indirect
/Lengthresolution, the page tree with attribute inheritance, and acontent-stream interpreter that tracks the CTM and text matrices, handles
TJkerning arrays (using large negative adjustments as word breaks), and recurses
into Form XObjects — which is where many producers put all the text.
Text decoding goes through each font's
/ToUnicodeCMap when present, andotherwise through WinAnsi/MacRoman/Standard encodings with
/Differencesresolved against a glyph-name table; Identity-H CID fonts are handled. Paragraph
structure is reconstructed from glyph positions: vertical jumps beyond a
multiple of the font size break paragraphs, smaller ones join lines, and
line-ending hyphens are removed when joining.
Embedded images are extracted — JPEG streams pass through untouched, and raw
rasters are re-encoded as PNG — so illustrated and scanned PDFs are readable
rather than empty.
Honest limitations, all of which degrade a page rather than fail a document:
JPX/CCITT/LZW-filtered content is skipped, and PDFs with a fully corrupt
cross-reference table are not reconstructed. A PDF with neither extractable text
nor usable images fails with a clear "no extractable text (scanned PDF?)"
message instead of producing an empty book.
Part 4 — Audio player
The M3 has an ES8388 codec and speaker that the stock firmware barely used.
AudioPlayerActivityplays MP3, WAV and FLAC — and gets AAC, M4A, Opus andVorbis for free from the same decoder. Decoding is
ESP32-audioI2S(the librarythe stock firmware itself used on this hardware); the SDK's
AudioManagerhandles only codec bring-up over I²C and analog volume, and its I²S path is
unused, so the two stacks don't overlap.
Decoding runs on its own pinned FreeRTOS task; every access to the decoder —
from that task and from the UI — is serialised behind one mutex, since the
library is not thread-safe. End-of-track is detected by polling rather than
callbacks, which keeps the integration stable across library versions.
The UI shows track name, position within the folder playlist, elapsed and total
time, a tap-to-seek progress bar, and prev/pause/next and volume rows. Folder
playlists auto-advance, volume persists (Settings and web API), and the device
will not auto-sleep mid-track. A new Audio section on the home screen opens
an audio-only browser, starting at
/Audio,/Audiobooksor/Musicwhen oneexists.
Gated behind
CROSSPOINT_AUDIO_PLAYER, so it costs nothing on devices withoutaudio hardware.
Correctness work
I don't have the hardware in a lab, so most of this was verified off-device
before it ever reached the panel:
MobiParser/MobiToEpub/PdfToEpub/ZipWritersources with thinfilesystem/logging shims, compiled with AddressSanitizer and
UndefinedBehaviorSanitizer.
with valid output; an illustrated 56-image MOBI yields 33 images where every
extracted image's decoded pixel dimensions match the dimensions the MOBI
markup declares for it — which verifies the record mapping, not merely that
bytes came out. Every generated EPUB is checked for ZIP and per-entry CRC
integrity and re-parsed with the same strict XML parser the device uses. Real
PDFs (19-page and 93-page) extract 37 K and 125 K characters of correct text.
corrupted files, treating a hang as a failure).
confirmed 14 real defects — all fixed. The most serious was a parser loop that
hung the device until the watchdog rebooted it, reachable from something as
ordinary as a URL inside an HTML comment; the fuzzer had found the same bug
independently. The rest were integer-overflow bounds checks (
offset + lengthwrapping past a size check, now written as subtraction), unbounded
file-controlled allocations, and unbounded loops over corrupt structures.
Untrusted-input hardening is deliberate throughout: these parsers read files
users download from anywhere, so bounds checks avoid wrapping arithmetic, every
loop is bounded, allocations derived from file fields are capped, and a corrupt
file fails with a message rather than crashing or hanging.
Testing on hardware
Verified on the device, iteratively, with a human at the panel: SD access, touch
and gestures across menus and lists, UI legibility at the new density, reading at
5–10 pt, text weight, page-turn quality, battery and charging indicators,
frontlight, sleep behaviour, EPUB rendering, MOBI conversion and reading (both
plain and illustrated), PDF conversion and reading, and audio playback.
Several fixes in this PR exist because of that loop rather than in spite of it:
the waveform work, the touch mapping, the UI density, the reader font sizes and
the ink floor were each tuned against what the panel actually showed, and the
list hit-test bug was found from a report that touch worked "in some menus but
not others".
Companion PR
The hardware layer this depends on is
Free-Ink/freeink-sdk#25.
This branch's
freeink-sdksubmodule points at that work.Prebuilt firmware
A built image with checksums and flashing instructions, for anyone who owns this
device and wants to try it before the port lands:
https://github.com/mr-tbot/crosspoint-reader/releases/tag/murphy-m3-v1