Compare commits

...
Author SHA1 Message Date
Justin Mitchell a86a644adc Reduce TLS memory requirements for KOReader sync
Split heap gate into separate free-memory (50KB) and largest-block (20KB) thresholds based on field measurements. Enable wolfSSL single-precision ECC to use fixed 256-bit arrays instead of heap-allocated fast-math bignums, reducing TLS handshake memory footprint. Reclaim ~7KB by right-sizing ESP timer task stacks and move WiFi code out of IRAM to free ~25-30KB for heap. Add memory audit landmarks for font and EPUB allocations.
2026-07-14 14:53:41 -04:00
Justin Mitchell 02398c8a96 Add memory profiling for EPUB and heap diagnostics
Adds residentBytes() and cssRuleCount() methods to track EPUB memory usage including parsed CSS. Enhances memory audit logging with EPUB heap usage, max allocatable block size, and CSS rule count. Implements one-shot heap block map dump on first settled audit to identify fragmentation and long-lived allocations.
2026-07-14 13:21:15 -04:00
Justin Mitchell ba92cb902e Add heap memory reporting for fonts and sections
Implements reportMemory() methods across SdCardFont, SdCardFontManager, and Section classes to track resident heap usage. Adds heap audit logging in EpubReaderActivity to attribute memory to fonts, framebuffer, section data, and other components for measurement-driven optimization.
2026-07-14 12:08:29 -04:00
Justin Mitchell 4d19d38152 Add idle-time glyph prewarming for next page
Preload glyphs for the next page during idle time to reduce page-turn latency. After a 400ms debounce period, scan the next page in FCM scan mode (no pixels drawn) to cache missing glyphs from SD card, avoiding ~100ms of SD reads during the actual page turn. Prewarm is deferred when rendering is active, heap is low, or during rapid page-flipping.
2026-07-14 11:26:07 -04:00
Justin Mitchell 302c0771dd Batch file list responses to reduce TCP segments
Buffer JSON file list entries into ~1.4KB batches before sending to avoid one TCP segment per file. This eliminates client delayed ACK delays that were causing large directory listings to take tens of seconds. Falls back to per-entry sends if buffer allocation fails.
2026-07-14 05:15:00 -04:00
Justin Mitchell c1a396c1ba Stop BLE before initializing WiFi in activities
Explicitly stop the BLE stack before bringing up WiFi in CrossPointWebServerActivity, FontDownloadActivity, and OtaUpdateActivity. On ESP32-C3, the shared radio and heap between BLE and WiFi stacks causes WiFi to be permanently starved if initialized while NimBLE's ~50KB is still resident, as the WiFi driver sizes its RX/TX buffer pools at init time.
2026-07-14 05:04:22 -04:00
Justin Mitchell 078d9ef535 Update status bar immediately on BLE connect/disconnect
Previously the status bar showed 'BT connecting' until the next page turn. Now monitors connection state in loop() and redraws the status bar immediately when connection completes or disconnects, restoring the chapter/book title. The connecting message now takes over the entire title slot instead of prepending to it.
2026-07-14 04:47:48 -04:00
Justin Mitchell 17e4230068 Add async display refresh to overlap CPU work
Introduces non-blocking display refresh methods that allow CPU work (like grayscale rendering) to overlap with the e-ink panel's refresh time. The async path starts the waveform and returns immediately, with the caller responsible for waiting via waitRefreshComplete(). Falls back to blocking refresh when fadingFix is enabled or the panel lacks deferral support.
2026-07-14 04:42:27 -04:00
Justin Mitchell 34e7ed249b Merge remote-tracking branch 'origin/develop' into feat-bluetooth 2026-07-14 04:20:50 -04:00
Justin Mitchell c5787d1cc1 fix: Use HALF_REFRESH for sleep and boot instead of FULL (#2588)
Changes all sleep screen display refreshes and boot screens from
FULL_REFRESH to HALF_REFRESH to match OEM firmware behavior. The stock
firmware uses a single-pass 0xD7 waveform for sleep screens, not the
multi-flash GC waveform (0xF7) that FULL_REFRESH triggers. For grayscale
mode, HALF_REFRESH is required because the gray nudge LUT is calibrated
against the pixel charge state left by the single-pass waveform; using
FULL_REFRESH causes blotchy noise in gray areas. Also adds X3 wb_gc
passive byte (the one-byte 0x54 to 0x00, matching stock's passive WB
table in the gc nudge bank): Every X3 grayscale render, reader AA,
covers, and the sleep-cover nudge, gets it.
2026-07-13 17:06:09 -04:00
Husam Younis 2a19eb9e0d feat: Arabic/Farsi/Urdu glyphs in built-in UI fonts - PR 2/3 (#2596) 2026-07-13 22:35:47 +03:00
Uri Tauber fae3423dec feat: add portuguese-PT.yaml (#2597) 2026-07-13 22:27:23 +03:00
Husam YounisandUri Tauber 932a472835 feat: Arabic/Farsi/Urdu bidi reordering and contextual shaping — PR 1/3 (#2541)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-07-13 16:02:21 +03:00
Jan Steinke 552b2683e6 fix: keep list item bullet inline with nested paragraph text (#2589) 2026-07-13 12:06:43 +03:00
Mr.Catfood bfa1d706b1 feat: Hidden wifi ssid support (#2360) 2026-07-13 02:33:48 -04:00
Jacob Latonis 73ab9ab20d feat: implement captive portal redirects for auto-loading the management page on hotspot mode (#2550) 2026-07-13 00:10:23 -04:00
Nick Perez 3a97f3e63e feat: Send optional document metadata with KOSync progress uploads (#1820) 2026-07-12 23:50:38 -04:00
Matthías Páll Gissurarson 39ea4b045f feat: render placeholders while waiting for images to render (#1003) 2026-07-12 21:40:07 +03:00
Justin Mitchell 444d87de82 fix: Add framebuffer release/realloc and improved lazy indexing (#2563) 2026-07-12 13:16:48 -04:00
Leopoldo Pla Sempere 859f6cb0d5 chore: update Spanish, Catalan, and Valencian Wi-Fi strings (#2578) 2026-07-12 12:13:06 +03:00
hooligan333andErica Jensen d8139e9605 perf: release CSS rule map after warm open (#2439)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:59:31 -04:00
hooligan333andErica Jensen b596f21e61 perf: stream NCX/NAV TOC into parser, drop temp-file round-trip (#2440)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:57:48 -04:00
hooligan333andErica Jensen 5fbc657aeb perf: always binary-search idref lookups in content.opf (#2433)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:56:28 -04:00
Jason Huebel bb06219c9c docs: add script to generate EPUB from USER_GUIDE.md (#2152) 2026-07-11 22:54:32 -04:00
Alexander Hoff ❍andAlexander Hoffer 5202bbf911 feat: auto-connect saved Wi-Fi networks (#2189)
Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
2026-07-11 22:47:26 -04:00
hooligan333andErica Jensen e0253a8664 perf: reserve CSS rule map before loading from cache (#2435)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:24:43 -04:00
hooligan333andErica Jensen e142e88ce7 perf: drop per-image delay(50) on chapter build, retry getDimensions (#2434)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
2026-07-11 22:23:52 -04:00
Timo 6add0b91bc feat: Add Finnish hyphenation (#2084) 2026-07-11 22:16:42 -04:00
Leopoldo Pla Sempere 287457f7dd fix: handle low-bit-depth, upscaled, and SVG EPUB images (#2503) 2026-07-11 21:45:53 -04:00
Justin Mitchell 3e627112f6 fix: oom exceptions for OPDS, KOSync, and OTA via wolfssl (#2475) 2026-07-12 01:53:26 +03:00
Stefan Blixten Karlsson 1f5669a08a fix: Swedish translation (#2577) 2026-07-11 23:20:20 +03:00
Ing. Jan Kaláb 0dc92c142c fix: update czech.yaml (#2574) 2026-07-11 21:09:09 +03:00
Leopoldo Pla Sempere 4b34a576eb chore: update Spanish, Catalan, and Valencian translations (#2566) 2026-07-10 17:45:40 +03:00
4fb843ef92 perf: skip redundant progress writes when position is unchanged (#2436)
Co-authored-by: Erica Jensen <erica@mailershaven.com>
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-07-10 10:37:43 +03:00
Justin Mitchell cecdbefa0e Normalize whitespace in comments and includes
Standardize spacing in inline comments and remove extra blank line in include statement for consistency
2026-07-09 00:21:25 -04:00
Justin Mitchell 4c5fd653c0 Add Bluetooth status icon to reader status bar
Display a Bluetooth icon in the status bar when BLE is connected. Also optimize memory management by lending the framebuffer instead of immediately tearing down BLE when heap is low, allowing BLE to remain active during rendering.
2026-07-09 00:13:54 -04:00
Justin Mitchell 05c1e9aa46 Add framebuffer lending for memory-intensive builds
Temporarily release framebuffer memory during section pagination to reduce heap pressure. The framebuffer can be released before memory-intensive operations and restored afterward, allowing BLE stack startup threshold to be lowered from 80KB to 70KB. Implements RAII-style FrameBufferBuildLoan class to ensure proper cleanup and automatic restart on restoration failure.
2026-07-09 00:04:11 -04:00
Justin Mitchell f3da5e4f06 Merge branch 'develop' of https://github.com/crosspoint-reader/crosspoint-reader into feat-bluetooth 2026-07-08 22:13:16 -04:00
Matteo Scopel a43061305a chore: update the Italian translation (#2559) 2026-07-08 22:22:47 +03:00
Uri Tauber 7ec07b4d9d fix: show "Failed to index" when failing to parse epub (#2556) 2026-07-08 21:22:35 +03:00
Uri Tauber c2c1badc7e chore: Refactor stores to use PersistableStore CRTP template (#2464) 2026-07-08 12:44:52 +03:00
Justin Mitchell 6f0707e83c Suppress cppcheck warnings for CI compatibility
Add missingInclude suppression to prevent false positives on fresh CI checkouts where include paths are unresolved. Add inline suppression for constVariableReference warning where cppcheck cannot detect mutations without full include path resolution.
2026-07-06 08:55:24 -04:00
Justin Mitchell 7b6df60a54 Shrink NimBLE footprint to prevent stack overflow
Reduce BLE stack memory usage from ~68KB to ~52KB by disabling unused NimBLE roles (peripheral, broadcaster), limiting connections to 1, and reducing buffer counts for ACL, HCI events, and ATT entries. Remove unused cloud components (esp_insights, esp_rainmaker) that require unavailable server certificates. These changes prevent stack collision with the render shed, eliminating restart flaps on the reader device.
2026-07-06 08:48:44 -04:00
Justin Mitchell 1c13913713 Implement keep-if-fits buffer reuse to reduce heap fragmentation
Replace per-page buffer free+realloc pattern with capacity tracking that only reallocates when needed size exceeds current capacity. This prevents heap fragmentation from non-coalescing holes that occurred when each page's freed block rarely fit the next page's allocation. After a few page turns, capacities converge on the book's maximum and page turns stop touching the allocator entirely.
2026-07-06 08:30:17 -04:00
Justin Mitchell 613371b716 Add heap pressure management for BLE and rendering
Prevent OOM aborts by shedding BLE stack when free heap drops below 24KB before rendering. Lower BLE start threshold from 100KB to 80KB to match steady-state heap levels (~84KB). Reserve vector capacity upfront in page parsing to avoid reallocation crashes on fragmented heaps. Gate background section builds on heap availability.
2026-07-06 08:12:11 -04:00
Justin Mitchell 0d7a84e3c5 Rename variable count to scanCount for clarity
Improves code readability by using a more descriptive variable name that better indicates the variable holds the device count from a BLE scan.
2026-07-06 02:23:04 -04:00
Justin Mitchell 4c93950771 Merge branch 'pr-2527' into feat-bluetooth
# Conflicts:
#	src/activities/reader/EpubReaderActivity.cpp
2026-07-06 02:10:55 -04:00
Justin Mitchell 1d01eebf55 Reformat Bluetooth scan log statement 2026-07-06 02:06:25 -04:00
Justin Mitchell 9bf6e5f43c Merge remote-tracking branch 'origin/develop' into feat-bluetooth
# Conflicts:
#	.gitmodules
#	freeink-sdk
#	platformio.ini
#	src/activities/reader/EpubReaderActivity.cpp
2026-07-06 02:04:38 -04:00
Nick ad3138983b fix: clear the BT-paused popup with an immediate redraw (toast semantics)
The popup lingered until the next page turn. E-ink has no free timers --
clearing costs a refresh whenever it happens -- so request the redraw
right away: the popup shows for the ~2 s page re-render, then clears.
2026-07-02 17:25:34 -05:00
Nick 793685e76b feat: user path to resume BT from low-memory pause via reader menu toggle
Field session: after a recovery teardown the heap settles at ~72 KB --
below the 100 KB start floor -- and never recovers on its own (the
defrag silent-restart only fires when a build FAILS, and builds succeed
now). BT stayed paused forever, the menu toggle claimed ON, and toggling
off/on changed nothing.

- Menu label tells the truth: ON / PAUSED (enabled but stack down) / OFF
  (new STR_STATE_PAUSED string)
- Toggling BT on below the heap floor silent-restarts into the current
  book: the fresh boot's ~118 KB passes the gate and BT auto-starts on
  resume. Explicit user intent is the right trigger for the defrag.
- Hoist the floor to bleinput::kStartMinFreeHeap, shared by the
  lifecycle gate and the toggle
2026-07-02 17:25:34 -05:00
Nick 551d29744a fix: stop BLE lifecycle oscillation after build recovery
The 70 KB restart floor was arithmetic nonsense: NimBLE takes ~57 KB, so
a restart at 70 KB free left ~13 KB -- below the 40 KB build pre-flight
-- so the next chapter build instantly re-entered recovery and tore BLE
back down. Field symptom: endless 'BT Connecting...' popup + redraw loop.

- Single conservative floor: 100 KB (57 KB stack + 40 KB build headroom)
- 30 s cool-down after any recovery teardown before the lifecycle may
  restart BLE, so a marginal heap can never flap; the 'BT paused (low
  memory)' popup shows instead and reading continues without the remote
2026-07-02 17:25:10 -05:00
Nick 74a0969cc6 fix: pre-flight heap floor before section builds; no inline BLE restart
Layout code (line-break DP arrays, CSS lookups, glyph buffers) allocates
via std::vector/std::string and abort()s on OOM under -fno-exceptions --
it cannot fail cleanly mid-build. Field crashes (X4, BLE resident):
builds entered at ~11 KB free and aborted in ParsedText::
computeLineBreaks; an inline BLE restart after recovery re-starved the
render and abort()ed in FontCacheManager's scanText_.reserve at ~8 KB.

- BUILD_MIN_FREE_HEAP (40 KB): pre-flight before the build; below the
  floor go straight to recovery instead of attempting a doomed build
- Recovery no longer restarts NimBLE inline; the lifecycle stays paused
  until the render completes (scoped unpause guard), then the main-loop
  lifecycle restarts BLE behind its own heap gate
2026-07-02 17:24:29 -05:00
Nick 06aa1ac2f7 feat: silent-restart defrag on section-build double failure + BLE debug flags
- EpubReaderActivity: when a section build fails even after the BLE
  teardown/retry, silentRestartToReader() as last-resort heap defrag,
  guarded by bootWasSilentRestart() to prevent reboot loops
- main/SilentRestart.h: expose bootWasSilentRestart()
- platformio.ini: enable FREEINK_BLE_HID_REPORT_DEBUG in env:default
2026-07-02 17:20:51 -05:00
Justin Mitchell c463da994e Add BLE lifecycle pause mechanism for memory mgmt
Introduces a global pause flag to temporarily block BLE stack auto-start during large memory allocations (e.g., in EPUB rendering). Also adds auto-restart logic for BLE scanning when device list is empty, and extensive debug logging for BLE scan lifecycle troubleshooting.
2026-06-30 16:31:54 -04:00
Justin Mitchell 988652513a Add FreeInk dependencies
Include BoardConfig, PowerManager, Rtc, Imu, FreeInkUI, and Icons libraries from the FreeInk SDK to support hardware abstraction layer functionality.
2026-06-24 17:41:36 -04:00
Justin Mitchell 28d0d6f5d2 Replace range-based for loop with std::fill
Use standard algorithm std::fill instead of manual loop to clear bleKeyMap array. This is more idiomatic C++ and expresses intent more clearly.
2026-06-24 17:15:44 -04:00
Justin Mitchell 568831f232 Update freeink-sdk submodule
Update freeink-sdk submodule from b026965 to a913bb3.
2026-06-24 17:10:58 -04:00
Justin Mitchell d6f5be6b7a Add keepsBluetoothAlive hook to Activity base class
Introduces a virtual method allowing activities to indicate whether they need the BLE stack to remain active. This enables selective teardown of Bluetooth to free heap memory, with the Bluetooth settings screen being able to override this for pairing and scanning functionality.
2026-06-24 17:00:52 -04:00
Justin Mitchell d30fde2f4d Add Bluetooth connecting popup message
Add new translation string for displaying a popup message while Bluetooth connection is in progress.
2026-06-24 16:21:49 -04:00
Justin Mitchell 6305777b22 Retry EPUB section build after freeing BLE stack
When building an EPUB section fails with Bluetooth enabled, temporarily stop the BLE stack to free up memory (~16KB), retry the build, then restart BLE. This works around memory fragmentation caused by the NimBLE stack that prevents allocating the large contiguous buffer needed for inflate/deflate operations. The recovery only runs once per uncached chapter since chapters are cached afterwards.
2026-06-24 16:13:43 -04:00
Justin Mitchell 9ab0b0bfb7 Add Bluetooth HID remote control support
Integrate BLE keyboard host functionality for page-turner remotes. Adds device pairing, button mapping, preset configurations for Free2/Free3 remotes, and persistent storage of mappings. Migrates from open-x4-sdk to freeink-sdk submodule which includes the BleKeyboardHost library. Implements CPU frequency locking during BLE operations to prevent watchdog timeouts.
2026-06-24 15:36:29 -04:00
172 changed files with 25385 additions and 7487 deletions
+6
View File
@@ -23,3 +23,9 @@ lib/EpdFont/scripts/output/
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out. # (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
.claude/* .claude/*
!.claude/skills/ !.claude/skills/
/managed_components
/.dummy
dependencies.lock
sdkconfig.default
sdkconfig.defaults
CMakeLists.txt
+1
View File
@@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
| grep -v -E '^lib/EpdFont/builtinFonts/' \ | grep -v -E '^lib/EpdFont/builtinFonts/' \
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \ | grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
| grep -v -E '^lib/uzlib/' \ | grep -v -E '^lib/uzlib/' \
| grep -v -E '^lib/miniz/third_party/' \
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i | xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
# Restore strict pipeline failure handling for the rest of the script. # Restore strict pipeline failure handling for the rest of the script.
set -o pipefail set -o pipefail
+9 -3
View File
@@ -90,13 +90,19 @@ if (parsedSize != fileSize) {
## `section.bin` ## `section.bin`
### Version 29 ### Version 30
Each file in `sections/*.bin` stores one laid-out spine section. The header is Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt. current reader settings, the section is discarded and rebuilt.
Version 29 includes: Version 30 is binary-identical to version 29. The version was bumped because
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
measures the shaped visual text), so word positions cached by v29 no longer
match what `drawText` renders.
Version 28 introduced serialized word style bits for underline, strikethrough,
superscript, and subscript. The format also includes:
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS, - cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
image rendering mode, and Focus Reading image rendering mode, and Focus Reading
@@ -119,7 +125,7 @@ import std.mem;
import std.string; import std.string;
import std.core; import std.core;
#define EXPECTED_VERSION 29 #define EXPECTED_VERSION 30
#define MAX_STRING_LENGTH 65535 #define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32 #define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96 #define FOOTNOTE_HREF_LEN 96
+8 -3
View File
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
## Join Network Mode ## Join Network Mode
1. Select **Join Network**. 1. Select **Join Network**.
2. Pick a 2.4 GHz Wi-Fi network from the scan results. 2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
3. Enter the password if prompted. connected network, then other visible saved networks in signal-strength
4. Save credentials if you want the reader to reconnect automatically next time. order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
the network list.
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
results.
4. Enter the password if prompted.
5. Save credentials if you want the reader to reconnect automatically next time.
After connection, the reader shows: After connection, the reader shows:
+5 -4
View File
@@ -44,16 +44,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
continue; continue;
} }
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0; const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
if (!isCombining && prevCp != 0) { if (!isCombining && prevCp != 0) {
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
} }
const int glyphBaseX = const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width) glyph->left, glyph->width)
: lastBaseX; : lastBaseX;
const int glyphBaseY = startY - raiseBy; const int glyphBaseY = startY - raiseBy;
*minX = std::min(*minX, glyphBaseX + glyph->left); *minX = std::min(*minX, glyphBaseX + glyph->left);
+76 -11
View File
@@ -36,24 +36,71 @@ namespace combiningMark {
constexpr int MIN_GAP_PX = 1; constexpr int MIN_GAP_PX = 1;
/// Compute the cursor-X at which to render a combining mark so its bitmap /// Placement of a mark relative to its base glyph. The default heuristic —
/// is visually centered over the base glyph's bitmap. /// centered over the base, raised clear of its top — suits Latin diacritics
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) { /// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft; /// on position: dagesh sits inside the letter body, the shin/sin dots
/// distinguish the letter by sitting over its right/left arm, and holam hangs
/// over the left corner. "Native" anchors keep the glyph's font-designed
/// height (which may overlap the base) instead of raising it.
enum class Anchor : uint8_t {
CenterRaised, ///< centered over the base, lifted above its top (default)
CenterNative, ///< centered over the base at font-native height
RightNative, ///< right edges aligned, font-native height
LeftNative, ///< left edges aligned, font-native height
};
constexpr Anchor anchorFor(const uint32_t cp) {
switch (cp) {
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
case 0x05BA: // holam haser for vav: straight above the vav stem
return Anchor::CenterNative;
case 0x05C1: // shin dot: over the letter's right arm
return Anchor::RightNative;
case 0x05B9: // holam: above the letter's left corner
case 0x05C2: // sin dot: over the letter's left arm
return Anchor::LeftNative;
default:
return Anchor::CenterRaised;
}
} }
/// Rotated-90CW variant of centerOver. In the rotated coordinate system /// Horizontal offset from the base bitmap's left edge to the mark bitmap's
/// left edge for a given anchor.
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
switch (anchor) {
case Anchor::LeftNative:
return 0;
case Anchor::RightNative:
return baseWidth - markWidth;
default:
return baseWidth / 2 - markWidth / 2;
}
}
/// Compute the cursor-X at which to render a combining mark so its bitmap
/// lands at its anchor position over the base glyph's bitmap.
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
const int markLeft, const int markWidth) {
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
}
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so /// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
/// every left/width term inverts sign. /// every left/width term inverts sign.
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) { constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft; const int baseWidth, const int markLeft, const int markWidth) {
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
} }
/// For combining marks that sit entirely above the baseline, compute how many /// For combining marks that sit entirely above the baseline, compute how many
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom /// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
/// edge and the top of the base glyph. Returns 0 for marks that extend to or /// edge and the top of the base glyph. Returns 0 for marks that extend to or
/// below the baseline (e.g. cedilla, dot-below, ogonek). /// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) { /// keep the font-native height (dagesh must stay inside the letter, the
/// shin/sin dots touch its arms).
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
if (anchor != Anchor::CenterRaised) return 0;
if (markTop - markHeight <= 0) return 0; if (markTop - markHeight <= 0) return 0;
const int gap = markTop - markHeight - baseTop; const int gap = markTop - markHeight - baseTop;
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0; return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
@@ -61,6 +108,20 @@ constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
} // namespace combiningMark } // namespace combiningMark
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
/// identical across both toolchains.
#if defined(_MSC_VER)
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
#define EPD_PACKED_END __pragma(pack(pop))
#define EPD_PACKED_ATTR
#else
#define EPD_PACKED_BEGIN
#define EPD_PACKED_END
#define EPD_PACKED_ATTR __attribute__((packed))
#endif
/// Fixed-point conventions used by EpdGlyph and EpdFontData: /// Fixed-point conventions used by EpdGlyph and EpdFontData:
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel) /// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel) /// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
@@ -95,17 +156,21 @@ typedef struct {
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search. /// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning). /// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
EPD_PACKED_BEGIN
typedef struct { typedef struct {
uint16_t codepoint; ///< Unicode codepoint uint16_t codepoint; ///< Unicode codepoint
uint8_t classId; ///< 1-based kerning class ID uint8_t classId; ///< 1-based kerning class ID
} __attribute__((packed)) EpdKernClassEntry; } EPD_PACKED_ATTR EpdKernClassEntry;
EPD_PACKED_END
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search. /// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup. /// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
EPD_PACKED_BEGIN
typedef struct { typedef struct {
uint32_t pair; ///< Packed codepoint pair (left << 16 | right) uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
} __attribute__((packed)) EpdLigaturePair; } EPD_PACKED_ATTR EpdLigaturePair;
EPD_PACKED_END
/// Data stored for FONT AS A WHOLE /// Data stored for FONT AS A WHOLE
typedef struct { typedef struct {
+5
View File
@@ -369,6 +369,11 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
} }
stats.pageBufferBytes += totalBytes; stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry); stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
// Landmark for the heap block map: page slots are the largest flash-font
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
(unsigned)glyphCount);
slot.fontData = fontData; slot.fontData = fontData;
slot.glyphCount = glyphCount; slot.glyphCount = glyphCount;
+81 -22
View File
@@ -68,6 +68,22 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c
const char* asCStr(const std::string& s) { return s.c_str(); } const char* asCStr(const std::string& s) { return s.c_str(); }
const char* asCStr(const char* s) { return s; } const char* asCStr(const char* s) { return s; }
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
// current capacity. Freeing + reallocating slightly different sizes every page
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
// next page's need), eroding the largest contiguous block all session. With
// reuse, capacities converge on the book's max page after a few turns and page
// turns stop touching the allocator. Only three small instantiations exist
// (interval/glyph/byte arrays), so template bloat is negligible.
template <typename T, typename CapT>
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
if (buf && capacity >= needed) return true;
delete[] buf;
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
capacity = buf ? static_cast<CapT>(needed) : 0;
return buf != nullptr;
}
} // namespace } // namespace
SdCardFont::~SdCardFont() { freeAll(); } SdCardFont::~SdCardFont() { freeAll(); }
@@ -83,6 +99,9 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
s.miniBitmap = nullptr; s.miniBitmap = nullptr;
s.miniIntervalCount = 0; s.miniIntervalCount = 0;
s.miniGlyphCount = 0; s.miniGlyphCount = 0;
s.miniIntervalCapacity = 0;
s.miniGlyphCapacity = 0;
s.miniBitmapCapacity = 0;
freeStyleMiniKern(s); freeStyleMiniKern(s);
memset(&s.miniData, 0, sizeof(s.miniData)); memset(&s.miniData, 0, sizeof(s.miniData));
s.epdFont.data = &s.stubData; s.epdFont.data = &s.stubData;
@@ -109,6 +128,9 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) {
s.miniKernRightEntryCount = 0; s.miniKernRightEntryCount = 0;
s.miniKernLeftClassCount = 0; s.miniKernLeftClassCount = 0;
s.miniKernRightClassCount = 0; s.miniKernRightClassCount = 0;
s.miniKernLeftCapacity = 0;
s.miniKernRightCapacity = 0;
s.miniKernMatrixCapacity = 0;
} }
void SdCardFont::freeStyleAll(PerStyle& s) { void SdCardFont::freeStyleAll(PerStyle& s) {
@@ -311,13 +333,13 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++; if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
} }
// Step 4: allocate the three mini buffers. The matrix is <1KB in practice // Step 4: size the three mini buffers (reused across pages when they fit; the
// (<30 × <30 × 1 byte) so fragmentation is a non-issue. // per-page sizes vary by a few entries, which as free+realloc churn was punching
// non-coalescing holes in the heap every page turn).
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight; const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount]; if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) ||
s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount]; !ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) ||
s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes]; !ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) {
if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) {
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u, LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
matrixBytes); matrixBytes);
freeStyleMiniKern(s); freeStyleMiniKern(s);
@@ -793,12 +815,19 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
return missed; return missed;
} }
// Build mini intervals from sorted codepoints // Build mini intervals from sorted codepoints. Reset counts and fall back to the
freeStyleMiniData(s); // stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
s.miniKernLeftEntryCount = 0;
s.miniKernRightEntryCount = 0;
s.miniKernLeftClassCount = 0;
s.miniKernRightClassCount = 0;
memset(&s.miniData, 0, sizeof(s.miniData));
s.epdFont.data = &s.stubData;
uint32_t intervalCapacity = validCount; if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) {
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
if (!s.miniIntervals) {
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx); LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
delete[] mappings; delete[] mappings;
return static_cast<int>(cpCount); return static_cast<int>(cpCount);
@@ -816,15 +845,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
} }
} }
// Allocate mini glyph array // Mini glyph array (reused across pages when it fits)
s.miniGlyphCount = validCount; if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) {
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
if (!s.miniGlyphs) {
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx); LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
delete[] mappings; delete[] mappings;
freeStyleMiniData(s); freeStyleMiniData(s);
return static_cast<int>(cpCount); return static_cast<int>(cpCount);
} }
s.miniGlyphCount = validCount;
// Build sorted read order for sequential I/O // Build sorted read order for sequential I/O
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount]; uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
@@ -891,8 +919,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
totalBitmapSize += s.miniGlyphs[i].dataLength; totalBitmapSize += s.miniGlyphs[i].dataLength;
} }
s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1]; if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
if (!s.miniBitmap) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx); LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
delete[] readOrder; delete[] readOrder;
delete[] mappings; delete[] mappings;
@@ -1195,7 +1222,8 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
} }
template <typename Iter> template <typename Iter>
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) { int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
const char* extraText) {
if (!loaded_) return -1; if (!loaded_) return -1;
styleMask = resolveStyleMask(styleMask); styleMask = resolveStyleMask(styleMask);
if (styleMask == 0) return 0; if (styleMask == 0) return 0;
@@ -1215,6 +1243,9 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
for (auto it = begin; it != end && !hitCap; ++it) { for (auto it = begin; it != end && !hitCap; ++it) {
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS); hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
} }
if (extraText && !hitCap) {
hitCap = collectUniqueCodepoints(extraText, codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
}
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; })) if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
codepoints[cpCount++] = ' '; codepoints[cpCount++] = ' ';
@@ -1232,12 +1263,13 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
return totalMissed; return totalMissed;
} }
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) { int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) {
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask); return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
} }
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) { int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask); const char* extraText) {
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
} }
// --- Stats --- // --- Stats ---
@@ -1374,6 +1406,33 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
return &self->overflow_[slot].glyph; return &self->overflow_[slot].glyph;
} }
size_t SdCardFont::reportMemory() const {
size_t total = 0;
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
const auto& s = styles_[si];
if (!s.present) continue;
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
// kept-if-fits mini arenas: capacity (not count) is what stays resident
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
total += fixed + mini + adv;
}
size_t overflowBytes = 0;
for (uint32_t i = 0; i < overflowCount_; ++i) {
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
}
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
return total;
}
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const { bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
for (uint32_t i = 0; i < overflowCount_; i++) { for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) return true; if (&overflow_[i].glyph == glyph) return true;
+29 -5
View File
@@ -47,9 +47,12 @@ class SdCardFont {
// Build a compact advance-only table for layout measurement. // Build a compact advance-only table for layout measurement.
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap), // Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
// batch-reads advanceX from SD, stores in a sorted per-style table. // batch-reads advanceX from SD, stores in a sorted per-style table.
// extraText: optional additional codepoints to warm in the same SD pass
// (e.g. shaped Arabic presentation forms the measurement path will look up).
// Returns number of codepoints not found in font coverage. // Returns number of codepoints not found in font coverage.
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F); int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F); int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
const char* extraText = nullptr);
// Look up advanceX for a codepoint from the advance table. // Look up advanceX for a codepoint from the advance table.
// Returns the 12.4 fixed-point advance, or 0 if not found. // Returns the 12.4 fixed-point advance, or 0 if not found.
@@ -101,6 +104,11 @@ class SdCardFont {
uint32_t uniqueGlyphs = 0; uint32_t uniqueGlyphs = 0;
uint32_t bitmapBytes = 0; uint32_t bitmapBytes = 0;
}; };
// MEMFIX-PORT: SD font resident-bytes audit; portable
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
// advance tables + overflow bitmaps) and return the total in bytes. Pure
// accounting — no allocation, no state change.
size_t reportMemory() const;
void logStats(const char* label = "SDCF"); void logStats(const char* label = "SDCF");
void resetStats(); void resetStats();
const Stats& getStats() const { return stats_; } const Stats& getStats() const { return stats_; }
@@ -140,11 +148,13 @@ class SdCardFont {
// Full intervals loaded from file (kept in RAM for codepoint lookup) // Full intervals loaded from file (kept in RAM for codepoint lookup)
EpdUnicodeInterval* fullIntervals = nullptr; EpdUnicodeInterval* fullIntervals = nullptr;
EPD_PACKED_BEGIN
struct BmpInterval16 { struct BmpInterval16 {
uint16_t first; uint16_t first;
uint16_t last; uint16_t last;
uint16_t offset; uint16_t offset;
} __attribute__((packed)); } EPD_PACKED_ATTR;
EPD_PACKED_END
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact"); static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
BmpInterval16* bmpIntervals = nullptr; BmpInterval16* bmpIntervals = nullptr;
bool intervalsAreBmp16 = false; bool intervalsAreBmp16 = false;
@@ -163,13 +173,22 @@ class SdCardFont {
// Stub EpdFontData returned when not prewarmed // Stub EpdFontData returned when not prewarmed
EpdFontData stubData{}; EpdFontData stubData{};
// Mini EpdFontData built during prewarm // Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages
// (capacities below track allocated sizes): freeing and reallocating slightly
// different sizes on every page turn was a primary heap fragmenter — each page's
// freed hole rarely fit the next page's need, so maxAlloc eroded all session.
// After a few pages the capacities converge on the book's max and page turns
// stop allocating entirely. freeStyleMiniData() still releases everything (and
// zeroes capacities) for style eviction / font unload.
EpdFontData miniData{}; EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr; EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr; EpdGlyph* miniGlyphs = nullptr;
uint8_t* miniBitmap = nullptr; uint8_t* miniBitmap = nullptr;
uint32_t miniIntervalCount = 0; uint32_t miniIntervalCount = 0;
uint32_t miniGlyphCount = 0; uint32_t miniGlyphCount = 0;
uint32_t miniIntervalCapacity = 0;
uint32_t miniGlyphCapacity = 0;
uint32_t miniBitmapCapacity = 0;
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full // Per-page mini kern matrix (built by buildMiniKernMatrix on each full
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints // prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
@@ -184,6 +203,10 @@ class SdCardFont {
uint8_t miniKernLeftClassCount = 0; uint8_t miniKernLeftClassCount = 0;
uint8_t miniKernRightClassCount = 0; uint8_t miniKernRightClassCount = 0;
int8_t* miniKernMatrix = nullptr; int8_t* miniKernMatrix = nullptr;
// Kept-if-fits capacities, same rationale as the mini glyph buffers above.
uint16_t miniKernLeftCapacity = 0;
uint16_t miniKernRightCapacity = 0;
uint32_t miniKernMatrixCapacity = 0;
// The EpdFont whose data pointer we manage // The EpdFont whose data pointer we manage
EpdFont epdFont{&stubData}; EpdFont epdFont{&stubData};
@@ -249,7 +272,8 @@ class SdCardFont {
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const; int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask); int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
template <typename Iter> template <typename Iter>
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask); int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
const char* extraText = nullptr);
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly); int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
// Global helpers // Global helpers
+8
View File
@@ -88,6 +88,14 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
loadedPointSize_ = 0; loadedPointSize_ = 0;
} }
size_t SdCardFontManager::reportMemory() const {
size_t total = 0;
for (const auto& lf : loaded_) {
if (lf.font) total += lf.font->reportMemory();
}
return total;
}
int SdCardFontManager::getFontId(const std::string& familyName) const { int SdCardFontManager::getFontId(const std::string& familyName) const {
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0; if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
return loaded_.front().fontId; return loaded_.front().fontId;
+4
View File
@@ -32,6 +32,10 @@ class SdCardFontManager {
// Get name of currently loaded family (empty if none). // Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; }; const std::string& currentFamilyName() const { return loadedFamilyName_; };
// MEMFIX-PORT: font manager audit passthrough; portable
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
size_t reportMemory() const;
// Point size that was actually loaded. // Point size that was actually loaded.
// 0 if nothing loaded. // 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; }; uint8_t currentPointSize() const { return loadedPointSize_; };
File diff suppressed because it is too large Load Diff
@@ -6,6 +6,8 @@
!NotoSerif/** !NotoSerif/**
!NotoSans/ !NotoSans/
!NotoSans/** !NotoSans/**
!NotoSansArabic/
!NotoSansArabic/**
!NotoSansHebrew/ !NotoSansHebrew/
!NotoSansHebrew/** !NotoSansHebrew/**
!OpenDyslexic/ !OpenDyslexic/
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+33 -3
View File
@@ -31,19 +31,48 @@ done
UI_FONT_SIZES=(10 12) UI_FONT_SIZES=(10 12)
UI_FONT_STYLES=("Regular" "Bold") UI_FONT_STYLES=("Regular" "Bold")
# Arabic glyphs for UI text (menus, file browser titles). The built-in fonts
# must cover the *output* of MiniBidi's do_shape() — contextual presentation
# forms — not base letters, or shaped UI text silently drops glyphs.
# Curated for firmware-size budget: core Arabic (Presentation Forms-B,
# incl. the Lam-Alef ligature forms) plus the Farsi/Urdu extra letters'
# Presentation Forms-A blocks, the few characters shaping leaves at their
# base codepoint, Arabic punctuation, and both digit sets. No harakat and
# no Sindhi/Pashto/Kurdish forms — book text gets those from SD-card fonts.
ARABIC_INTERVALS=(
--additional-intervals 0x060C,0x060C # Arabic comma
--additional-intervals 0x061B,0x061B # Arabic semicolon
--additional-intervals 0x061F,0x061F # Arabic question mark
--additional-intervals 0x0621,0x0621 # hamza (non-joining, never shaped)
--additional-intervals 0x0640,0x0640 # tatweel
--additional-intervals 0x0660,0x0669 # Arabic-Indic digits
--additional-intervals 0x06BA,0x06BA # noon ghunna base (initial/medial keep base cp)
--additional-intervals 0x06D4,0x06D4 # Urdu full stop
--additional-intervals 0x06F0,0x06F9 # extended Arabic-Indic digits (Farsi/Urdu)
--additional-intervals 0xFB56,0xFB59 # peh (Farsi)
--additional-intervals 0xFB66,0xFB69 # tteh (Urdu)
--additional-intervals 0xFB7A,0xFB7D # tcheh (Farsi)
--additional-intervals 0xFB88,0xFB95 # ddal, jeh, rreh (Urdu), keheh, gaf (Farsi/Urdu)
--additional-intervals 0xFB9E,0xFB9F # noon ghunna isolated/final (Urdu)
--additional-intervals 0xFBA6,0xFBB1 # heh goal, heh doachashmee, yeh barree(+hamza) (Urdu)
--additional-intervals 0xFBFC,0xFBFF # farsi yeh (Farsi/Urdu)
--additional-intervals 0xFE80,0xFEFC # Presentation Forms-B: core Arabic + Lam-Alef
)
for size in ${UI_FONT_SIZES[@]}; do for size in ${UI_FONT_SIZES[@]}; do
for style in ${UI_FONT_STYLES[@]}; do for style in ${UI_FONT_STYLES[@]}; do
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')" font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf" font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf" hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
arabic_path="../builtinFonts/source/NotoSansArabic/NotoSansArabic-${style}.ttf"
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for # Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs # Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
# are filled from it while every glyph Ubuntu already has stays unchanged # are filled from it while every glyph Ubuntu already has stays unchanged
# (fontstack is ordered by descending priority). # (fontstack is ordered by descending priority).
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf" viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
output_path="../builtinFonts/${font_name}.h" output_path="../builtinFonts/${font_name}.h"
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \ python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path $viet_path \
--additional-intervals 0x05D0,0x05EA > $output_path --additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path
echo "Generated $output_path" echo "Generated $output_path"
done done
done done
@@ -51,7 +80,8 @@ done
python fontconvert.py notosans_8_regular 8 \ python fontconvert.py notosans_8_regular 8 \
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \ ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \ ../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
--additional-intervals 0x05D0,0x05EA > ../builtinFonts/notosans_8_regular.h ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf \
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > ../builtinFonts/notosans_8_regular.h
echo "" echo ""
echo "Running compression verification..." echo "Running compression verification..."
+19 -59
View File
@@ -153,18 +153,11 @@ bool Epub::parseTocNcxFile() const {
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str()); LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
const auto tmpNcxPath = getCachePath() + "/toc.ncx"; size_t ncxSize;
HalFile tempNcxFile; if (!getItemSize(tocNcxItem, &ncxSize)) {
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { LOG_ERR("EBP", "Could not get size of toc ncx file");
return false; return false;
} }
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
// Explicitly close() file before reopening for reading
tempNcxFile.close();
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
return false;
}
const auto ncxSize = tempNcxFile.size();
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get()); TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
@@ -173,29 +166,13 @@ bool Epub::parseTocNcxFile() const {
return false; return false;
} }
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024)); // Stream the decompressed NCX straight into the parser instead of round-tripping
if (!ncxBuffer) { // through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser"); if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
LOG_ERR("EBP", "Could not read toc ncx file");
return false; return false;
} }
while (tempNcxFile.available()) {
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
if (readSize == 0) break;
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
if (processedSize != readSize) {
LOG_ERR("EBP", "Could not process all toc ncx data");
free(ncxBuffer);
return false;
}
}
free(ncxBuffer);
// Explicitly close() file before calling Storage.remove()
tempNcxFile.close();
Storage.remove(tmpNcxPath.c_str());
LOG_DBG("EBP", "Parsed TOC items"); LOG_DBG("EBP", "Parsed TOC items");
return true; return true;
} }
@@ -209,18 +186,11 @@ bool Epub::parseTocNavFile() const {
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str()); LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
const auto tmpNavPath = getCachePath() + "/toc.nav"; size_t navSize;
HalFile tempNavFile; if (!getItemSize(tocNavItem, &navSize)) {
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { LOG_ERR("EBP", "Could not get size of toc nav file");
return false; return false;
} }
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
// Explicitly close() file before reopening for reading
tempNavFile.close();
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
return false;
}
const auto navSize = tempNavFile.size();
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf // Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
// and the HTMLX nav file will have hrefs relative to itself // and the HTMLX nav file will have hrefs relative to itself
@@ -232,28 +202,13 @@ bool Epub::parseTocNavFile() const {
return false; return false;
} }
const auto navBuffer = static_cast<uint8_t*>(malloc(1024)); // Stream the decompressed nav document straight into the parser instead of round-tripping
if (!navBuffer) { // through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
LOG_ERR("EBP", "Could not allocate memory for toc nav parser"); if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
LOG_ERR("EBP", "Could not read toc nav file");
return false; return false;
} }
while (tempNavFile.available()) {
const auto readSize = tempNavFile.read(navBuffer, 1024);
const auto processedSize = navParser.write(navBuffer, readSize);
if (processedSize != readSize) {
LOG_ERR("EBP", "Could not process all toc nav data");
free(navBuffer);
return false;
}
}
free(navBuffer);
// Explicitly close() file before calling Storage.remove()
tempNavFile.close();
Storage.remove(tmpNavPath.c_str());
LOG_DBG("EBP", "Parsed TOC nav items"); LOG_DBG("EBP", "Parsed TOC nav items");
return true; return true;
} }
@@ -396,6 +351,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
Storage.removeDir((cachePath + "/sections").c_str()); Storage.removeDir((cachePath + "/sections").c_str());
} }
} }
// Release the resolved CSS rule map: it is only needed transiently while building
// section caches, and createSectionFile reloads it from cache on demand. Holding it
// resident pins tens of KB for the whole reading session (more on warm resume into
// an already-cached chapter, where createSectionFile never runs to clear it).
cssParser->clear();
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str()); LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
return true; return true;
} }
+12
View File
@@ -44,6 +44,18 @@ class Epub {
} }
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
// Approximate resident heap of the open book (audit): path strings, the CSS
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
// (counts + HalFile handles) and contributes little.
size_t residentBytes() const {
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
contentBasePath.capacity() + cachePath.capacity();
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
if (cssParser) total += cssParser->residentBytes();
return total;
}
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
bool load(bool buildIfMissing = true, bool skipLoadingCss = false); bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
+129 -58
View File
@@ -1,5 +1,6 @@
#include "BookMetadataCache.h" #include "BookMetadataCache.h"
#include <BufferedFile.h>
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <Utf8.h> #include <Utf8.h>
@@ -14,6 +15,52 @@ constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-com
constexpr char bookBinFile[] = "/book.bin"; constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on
// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash.
constexpr size_t BUILD_IO_BUFFER_SIZE = 4096;
// Entry (de)serializers, templated so they run over HalFile and the Buffered*
// wrappers alike (two instantiations each -- a few hundred bytes of flash, in
// exchange for the build path streaming at SD speed instead of per-pod).
template <typename F>
uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) {
const uint32_t pos = file.position();
serialization::writeString(file, entry.href);
serialization::writePod(file, entry.cumulativeSize);
serialization::writePod(file, entry.tocIndex);
return pos;
}
template <typename F>
uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) {
const uint32_t pos = file.position();
serialization::writeString(file, entry.title);
serialization::writeString(file, entry.href);
serialization::writeString(file, entry.anchor);
serialization::writePod(file, entry.level);
serialization::writePod(file, entry.spineIndex);
return pos;
}
template <typename F>
BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) {
BookMetadataCache::SpineEntry entry;
serialization::readString(file, entry.href);
serialization::readPod(file, entry.cumulativeSize);
serialization::readPod(file, entry.tocIndex);
return entry;
}
template <typename F>
BookMetadataCache::TocEntry readTocEntryFrom(F& file) {
BookMetadataCache::TocEntry entry;
serialization::readString(file, entry.title);
serialization::readString(file, entry.href);
serialization::readString(file, entry.anchor);
serialization::readPod(file, entry.level);
serialization::readPod(file, entry.spineIndex);
return entry;
}
} // namespace } // namespace
/* ============= WRITING / BUILDING FUNCTIONS ================ */ /* ============= WRITING / BUILDING FUNCTIONS ================ */
@@ -30,13 +77,23 @@ bool BookMetadataCache::beginContentOpfPass() {
LOG_DBG("BMC", "Beginning content opf pass"); LOG_DBG("BMC", "Beginning content opf pass");
// Open spine file for writing // Open spine file for writing
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) {
return false;
}
// Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes.
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(spineFile, BUILD_IO_BUFFER_SIZE);
return true;
} }
bool BookMetadataCache::endContentOpfPass() { bool BookMetadataCache::endContentOpfPass() {
const bool flushed = !passOut || passOut->flush();
passOut.reset();
// Explicit close() required: member variable persists beyond function scope // Explicit close() required: member variable persists beyond function scope
spineFile.close(); spineFile.close();
return true; if (!flushed) {
LOG_ERR("BMC", "Failed writing spine tmp file");
}
return flushed;
} }
bool BookMetadataCache::beginTocPass() { bool BookMetadataCache::beginTocPass() {
@@ -74,10 +131,17 @@ bool BookMetadataCache::beginTocPass() {
useSpineHrefIndex = false; useSpineHrefIndex = false;
} }
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
return true; return true;
} }
bool BookMetadataCache::endTocPass() { bool BookMetadataCache::endTocPass() {
const bool flushed = !passOut || passOut->flush();
passOut.reset();
if (!flushed) {
LOG_ERR("BMC", "Failed writing toc tmp file");
}
// Explicit close() required: member variables persist beyond function scope // Explicit close() required: member variables persist beyond function scope
tocFile.close(); tocFile.close();
spineFile.close(); spineFile.close();
@@ -86,7 +150,7 @@ bool BookMetadataCache::endTocPass() {
spineHrefIndex.shrink_to_fit(); spineHrefIndex.shrink_to_fit();
useSpineHrefIndex = false; useSpineHrefIndex = false;
return true; return flushed;
} }
bool BookMetadataCache::endWrite() { bool BookMetadataCache::endWrite() {
@@ -119,6 +183,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
return false; return false;
} }
// Buffered streams for the whole build: every access below is sequential per
// file, but interleaved ACROSS files, which thrashes SdFat's single shared
// sector cache when unbuffered (one 512B SD transaction per 4-byte pod --
// measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return.
serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE);
serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE);
serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE);
constexpr uint32_t headerASize = constexpr uint32_t headerASize =
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
@@ -128,31 +200,34 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
const uint32_t lutOffset = headerASize + metadataSize; const uint32_t lutOffset = headerASize + metadataSize;
// Header A // Header A
serialization::writePod(bookFile, BOOK_CACHE_VERSION); serialization::writePod(bookOut, BOOK_CACHE_VERSION);
serialization::writePod(bookFile, lutOffset); serialization::writePod(bookOut, lutOffset);
serialization::writePod(bookFile, spineCount); serialization::writePod(bookOut, spineCount);
serialization::writePod(bookFile, tocCount); serialization::writePod(bookOut, tocCount);
// Metadata // Metadata
serialization::writeString(bookFile, metadata.title); serialization::writeString(bookOut, metadata.title);
serialization::writeString(bookFile, metadata.author); serialization::writeString(bookOut, metadata.author);
serialization::writeString(bookFile, metadata.language); serialization::writeString(bookOut, metadata.language);
serialization::writeString(bookFile, metadata.coverItemHref); serialization::writeString(bookOut, metadata.coverItemHref);
serialization::writeString(bookFile, metadata.textReferenceHref); serialization::writeString(bookOut, metadata.textReferenceHref);
// Loop through spine entries, writing LUT positions // Loop through spine entries, writing LUT positions
spineFile.seek(0); spineIn.seek(0);
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
uint32_t pos = spineFile.position(); const uint32_t pos = spineIn.position();
auto spineEntry = readSpineEntry(spineFile); readSpineEntryFrom(spineIn);
serialization::writePod(bookFile, pos + lutOffset + lutSize); serialization::writePod(bookOut, pos + lutOffset + lutSize);
} }
// Total size of the spine tmp file: entries land in book.bin after the toc LUT
// and the full spine block, so toc LUT positions are offset by it.
const auto spineBytes = static_cast<uint32_t>(spineIn.position());
// Loop through toc entries, writing LUT positions // Loop through toc entries, writing LUT positions
tocFile.seek(0); tocIn.seek(0);
for (int i = 0; i < tocCount; i++) { for (int i = 0; i < tocCount; i++) {
uint32_t pos = tocFile.position(); const uint32_t pos = tocIn.position();
auto tocEntry = readTocEntry(tocFile); readTocEntryFrom(tocIn);
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position())); serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
} }
// LUTs complete // LUTs complete
@@ -160,9 +235,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m)) // Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
std::deque<int16_t> spineToTocIndex(spineCount, -1); std::deque<int16_t> spineToTocIndex(spineCount, -1);
tocFile.seek(0); tocIn.seek(0);
for (int j = 0; j < tocCount; j++) { for (int j = 0; j < tocCount; j++) {
auto tocEntry = readTocEntry(tocFile); auto tocEntry = readTocEntryFrom(tocIn);
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) { if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
if (spineToTocIndex[tocEntry.spineIndex] == -1) { if (spineToTocIndex[tocEntry.spineIndex] == -1) {
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j); spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
@@ -197,9 +272,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
std::deque<ZipFile::SizeTarget> targets; std::deque<ZipFile::SizeTarget> targets;
targets.resize(spineCount); targets.resize(spineCount);
spineFile.seek(0); spineIn.seek(0);
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
auto entry = readSpineEntry(spineFile); auto entry = readSpineEntryFrom(spineIn);
std::string path = FsHelpers::normalisePath(entry.href); std::string path = FsHelpers::normalisePath(entry.href);
ZipFile::SizeTarget t; ZipFile::SizeTarget t;
@@ -224,10 +299,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
} }
uint32_t cumSize = 0; uint32_t cumSize = 0;
spineFile.seek(0); spineIn.seek(0);
int lastSpineTocIndex = -1; int lastSpineTocIndex = -1;
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
auto spineEntry = readSpineEntry(spineFile); auto spineEntry = readSpineEntryFrom(spineIn);
spineEntry.tocIndex = spineToTocIndex[i]; spineEntry.tocIndex = spineToTocIndex[i];
@@ -260,23 +335,33 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
spineEntry.cumulativeSize = cumSize; spineEntry.cumulativeSize = cumSize;
// Write out spine data to book.bin // Write out spine data to book.bin
writeSpineEntry(bookFile, spineEntry); writeSpineEntryTo(bookOut, spineEntry);
} }
// Close opened zip file // Close opened zip file
zip.close(); zip.close();
// Loop through toc entries from toc file writing to book.bin // Loop through toc entries from toc file writing to book.bin
tocFile.seek(0); tocIn.seek(0);
for (int i = 0; i < tocCount; i++) { for (int i = 0; i < tocCount; i++) {
auto tocEntry = readTocEntry(tocFile); auto tocEntry = readTocEntryFrom(tocIn);
writeTocEntry(bookFile, tocEntry); writeTocEntryTo(bookOut, tocEntry);
} }
const bool written = bookOut.flush();
// Explicit close() required: member variables persist beyond function scope // Explicit close() required: member variables persist beyond function scope
bookFile.close(); bookFile.close();
spineFile.close(); spineFile.close();
tocFile.close(); tocFile.close();
if (!written) {
// A short write (card full/removed) would leave a truncated book.bin that
// still passes the version check on load; remove it so the next open rebuilds.
LOG_ERR("BMC", "Failed writing book.bin, removing truncated file");
Storage.remove((cachePath + bookBinFile).c_str());
return false;
}
LOG_DBG("BMC", "Successfully built book.bin"); LOG_DBG("BMC", "Successfully built book.bin");
return true; return true;
} }
@@ -294,21 +379,11 @@ bool BookMetadataCache::cleanupTmpFiles() const {
} }
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const { uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
const uint32_t pos = file.position(); return writeSpineEntryTo(file, entry);
serialization::writeString(file, entry.href);
serialization::writePod(file, entry.cumulativeSize);
serialization::writePod(file, entry.tocIndex);
return pos;
} }
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const { uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
const uint32_t pos = file.position(); return writeTocEntryTo(file, entry);
serialization::writeString(file, entry.title);
serialization::writeString(file, entry.href);
serialization::writeString(file, entry.anchor);
serialization::writePod(file, entry.level);
serialization::writePod(file, entry.spineIndex);
return pos;
} }
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called // Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
@@ -320,7 +395,11 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
} }
const SpineEntry entry(href, 0, -1); const SpineEntry entry(href, 0, -1);
writeSpineEntry(spineFile, entry); if (passOut) {
writeSpineEntryTo(*passOut, entry);
} else {
writeSpineEntry(spineFile, entry);
}
spineCount++; spineCount++;
} }
@@ -368,7 +447,11 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
// Compose the title to NFC at index time so the cache stores precomposed glyphs; // Compose the title to NFC at index time so the cache stores precomposed glyphs;
// device fonts have no combining-mark positioning, so NFD titles render broken. // device fonts have no combining-mark positioning, so NFD titles render broken.
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex); const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
writeTocEntry(tocFile, entry); if (passOut) {
writeTocEntryTo(*passOut, entry);
} else {
writeTocEntry(tocFile, entry);
}
tocCount++; tocCount++;
} }
@@ -442,19 +525,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
} }
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const { BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
SpineEntry entry; return readSpineEntryFrom(file);
serialization::readString(file, entry.href);
serialization::readPod(file, entry.cumulativeSize);
serialization::readPod(file, entry.tocIndex);
return entry;
} }
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
TocEntry entry;
serialization::readString(file, entry.title);
serialization::readString(file, entry.href);
serialization::readString(file, entry.anchor);
serialization::readPod(file, entry.level);
serialization::readPod(file, entry.spineIndex);
return entry;
}
+7
View File
@@ -1,9 +1,11 @@
#pragma once #pragma once
#include <BufferedFile.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <algorithm> #include <algorithm>
#include <deque> #include <deque>
#include <memory>
#include <string> #include <string>
class BookMetadataCache { class BookMetadataCache {
@@ -54,6 +56,11 @@ class BookMetadataCache {
// Temp file handles during build // Temp file handles during build
HalFile spineFile; HalFile spineFile;
HalFile tocFile; HalFile tocFile;
// Buffers the per-entry tmp-file writes during the OPF/TOC passes: those
// writes interleave with zip-inflate SD reads, and unbuffered they thrash
// SdFat's shared sector cache (one 512B transaction per 4-byte pod). One
// wrapper serves whichever pass is active (spine, then toc).
std::unique_ptr<serialization::BufferedFileWriter> passOut;
// Index for fast href→spineIndex lookup (used only for large EPUBs) // Index for fast href→spineIndex lookup (used only for large EPUBs)
struct SpineHrefIndexEntry { struct SpineHrefIndexEntry {
+19
View File
@@ -57,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset); imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
} }
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
}
bool PageImage::serialize(HalFile& file) { bool PageImage::serialize(HalFile& file) {
serialization::writePod(file, xPos); serialization::writePod(file, xPos);
serialization::writePod(file, yPos); serialization::writePod(file, yPos);
@@ -125,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs
[](const PageElement& element) { return element.getTag() == TAG_PageImage; }); [](const PageElement& element) { return element.getTag() == TAG_PageImage; });
} }
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
const int yOffset) const {
for (const auto& element : elements) {
if (element->getTag() == TAG_PageImage) {
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
} else {
element->render(renderer, fontId, xOffset, yOffset);
}
}
}
bool Page::serialize(HalFile& file) const { bool Page::serialize(HalFile& file) const {
const uint16_t count = elements.size(); const uint16_t count = elements.size();
serialization::writePod(file, count); serialization::writePod(file, count);
@@ -158,6 +173,10 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
uint16_t count; uint16_t count;
serialization::readPod(file, count); serialization::readPod(file, count);
// Reserve up front: growth-by-doubling needs old + new capacity live at once and
// reallocates repeatedly — a field crash (bad_alloc -> abort under -fno-exceptions)
// hit exactly this append path on a heavily fragmented heap.
page->elements.reserve(count);
for (uint16_t i = 0; i < count; i++) { for (uint16_t i = 0; i < count; i++) {
uint8_t tag; uint8_t tag;
+9
View File
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos) PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), imageBlock(std::move(block)) {} : PageElement(xPos, yPos), imageBlock(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
bool serialize(HalFile& file) override; bool serialize(HalFile& file) override;
PageElementTag getTag() const override { return TAG_PageImage; } PageElementTag getTag() const override { return TAG_PageImage; }
static std::unique_ptr<PageImage> deserialize(HalFile& file); static std::unique_ptr<PageImage> deserialize(HalFile& file);
@@ -89,6 +90,7 @@ class Page {
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(HalFile& file) const; bool serialize(HalFile& file) const;
static std::unique_ptr<Page> deserialize(HalFile& file); static std::unique_ptr<Page> deserialize(HalFile& file);
@@ -98,6 +100,13 @@ class Page {
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; }); [](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
} }
bool hasImagesNeedingDecode() const {
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
return element->getTag() == TAG_PageImage &&
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
});
}
// Get bounding box of all images on the page (union of image rects) // Get bounding box of all images on the page (union of image rects)
// Returns false if no images. Coordinates are relative to page origin. // Returns false if no images. Coordinates are relative to page origin.
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const { bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
+5 -1
View File
@@ -11,9 +11,13 @@
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace { namespace {
// v28: text decoration bits now include line-through in serialized wordStyles.
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated // v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
// text blob) instead of length-prefixed strings and per-field arrays. // text blob) instead of length-prefixed strings and per-field arrays.
constexpr uint8_t SECTION_FILE_VERSION = 29; // v30: Arabic shaping changed both drawing and measurement (getTextAdvanceX now
// measures the shaped visual text); cached word positions from v29 no longer
// match what drawText renders.
constexpr uint8_t SECTION_FILE_VERSION = 30;
// Written into the version field while a build is in progress; patched to // Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned / // SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects // crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
+15
View File
@@ -72,6 +72,7 @@ class Section {
// Builds write here and are swapped over filePath only on commit, so a prior // Builds write here and are swapped over filePath only on commit, so a prior
// partial/finalized file stays readable while a rebuild is in progress. // partial/finalized file stays readable while a rebuild is in progress.
std::string binTmpPath() const { return filePath + ".part"; } std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const; std::unique_ptr<Page> loadPageAt(int page) const;
// Read a page already laid out by the in-progress build (page < build LUT size), from // Read a page already laid out by the in-progress build (page < build LUT size), from
// the partially-written tmp .bin without disturbing the build's write cursor. // the partially-written tmp .bin without disturbing the build's write cursor.
@@ -131,6 +132,20 @@ class Section {
// (covers finalized sections and partials from a previous session). // (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const; std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// MEMFIX-PORT: section resident-bytes audit accessor; portable
// Approximate resident heap for the audit log. Steady state (no build) a
// Section holds little beyond itself; during a build the page LUT and path
// strings dominate (the parser's internal footprint is not walked here).
size_t residentBytes() const {
size_t total = sizeof(Section) + filePath.capacity();
if (build_) {
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
}
return total;
}
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a // True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild. // giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
bool hasHtmlCache() const; bool hasHtmlCache() const;
+87 -10
View File
@@ -5,6 +5,8 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <cstdlib>
#include "Epub/converters/DirectPixelWriter.h" #include "Epub/converters/DirectPixelWriter.h"
#include "Epub/converters/ImageDecoderFactory.h" #include "Epub/converters/ImageDecoderFactory.h"
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
return imagePath + ".pxc"; return imagePath + ".pxc";
} }
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
uint16_t& cachedHeight) {
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
return false;
}
const int widthDiff = abs(cachedWidth - expectedWidth);
const int heightDiff = abs(cachedHeight - expectedHeight);
if (widthDiff > 1 || heightDiff > 1) {
return false;
}
const size_t bytesPerRow = (cachedWidth + 3) / 4;
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
return cacheFile.size() >= expectedSize;
}
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
// record so an image that failed renders its placeholder directly for the rest
// of the reader session instead of paying another placeholder refresh and
// decode. The reader clears this on entry so transient memory/storage failures
// are retried.
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
size_t failedImageCount = 0;
uint64_t imagePathHash(const std::string& path) {
uint64_t hash = 14695981039346656037ull;
for (const char c : path) {
hash ^= static_cast<uint8_t>(c);
hash *= 1099511628211ull;
}
return hash;
}
bool imageFailedThisSession(const std::string& path) {
const uint64_t hash = imagePathHash(path);
for (size_t i = 0; i < failedImageCount; i++) {
if (failedImageHashes[i] == hash) return true;
}
return false;
}
void rememberImageFailure(const std::string& path) {
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
failedImageHashes[failedImageCount++] = imagePathHash(path);
}
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth, bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
int expectedHeight) { int expectedHeight) {
HalFile cacheFile; HalFile cacheFile;
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
} }
uint16_t cachedWidth, cachedHeight; uint16_t cachedWidth, cachedHeight;
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
return false; LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
}
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
int widthDiff = abs(cachedWidth - expectedWidth);
int heightDiff = abs(cachedHeight - expectedHeight);
if (widthDiff > 1 || heightDiff > 1) {
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
expectedHeight);
return false; return false;
} }
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
} // namespace } // namespace
bool ImageBlock::hasValidCache() const {
const auto cachePath = getCachePath(imagePath);
HalFile cacheFile;
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
return false;
}
uint16_t cachedWidth, cachedHeight;
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
}
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
renderer.fillRect(x, y, width, height, true);
if (width > 2 && height > 2) {
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
}
}
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
// The font-prewarm scan pass only accumulates glyphs; an image contributes // The font-prewarm scan pass only accumulates glyphs; an image contributes
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode // none, and its DirectPixelWriter output bypasses the renderer's scan-mode
@@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
return; return;
} }
if (imageFailedThisSession(imagePath)) {
renderPlaceholder(renderer, x, y);
return;
}
// Try to render from cache first // Try to render from cache first
std::string cachePath = getCachePath(imagePath); std::string cachePath = getCachePath(imagePath);
if (renderFromCache(renderer, cachePath, x, y, width, height)) { if (renderFromCache(renderer, cachePath, x, y, width, height)) {
@@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
HalFile file; HalFile file;
if (!Storage.openFileForRead("IMG", imagePath, file)) { if (!Storage.openFileForRead("IMG", imagePath, file)) {
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str()); LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
size_t fileSize = file.size(); size_t fileSize = file.size();
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
if (fileSize == 0) { if (fileSize == 0) {
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str()); LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath); ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
if (!decoder) { if (!decoder) {
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str()); LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config); bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
if (!success) { if (!success) {
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str()); LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
rememberImageFailure(imagePath);
renderPlaceholder(renderer, x, y);
return; return;
} }
+4
View File
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
int16_t getHeight() const { return height; } int16_t getHeight() const { return height; }
bool imageExists() const; bool imageExists() const;
bool hasValidCache() const;
bool needsDecode() const;
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
static void clearSessionRenderFailures();
BlockType getType() override { return IMAGE_BLOCK; } BlockType getType() override { return IMAGE_BLOCK; }
bool isEmpty() override { return false; } bool isEmpty() override { return false; }
@@ -99,24 +99,58 @@ int bytesPerPixelFromType(int pixelType) {
} }
} }
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) { int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; }
int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) {
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin. // +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
int pitch = srcWidth * bytesPerPixelFromType(pixelType); int pitch = srcWidth * bytesPerPixelFromType(pixelType);
if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) {
pitch = packedRowBytes(srcWidth, bitsPerSample);
}
return ((pitch + 1) * 2) + 32; return ((pitch + 1) * 2) + 32;
} }
bool isSupportedBitDepth(int pixelType, int bitsPerSample) {
if (bitsPerSample == 8) return true;
if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false;
return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED;
}
uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) {
if (bitsPerSample == 8) return pixels[x];
const int bitOffset = x * bitsPerSample;
const int shift = 8 - bitsPerSample - (bitOffset & 7);
const uint8_t mask = (1U << bitsPerSample) - 1;
return (pixels[bitOffset >> 3] >> shift) & mask;
}
uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
if (bitsPerSample == 8) return sample;
const uint8_t maxSample = (1U << bitsPerSample) - 1;
return static_cast<uint8_t>((sample * 255U) / maxSample);
}
// Convert entire source line to grayscale with alpha blending to white background. // Convert entire source line to grayscale with alpha blending to white background.
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards. // For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead. // Processing the whole line at once improves cache locality and reduces per-pixel overhead.
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) { void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
uint8_t* palette, int hasAlpha) {
switch (pixelType) { switch (pixelType) {
case PNG_PIXEL_GRAYSCALE: case PNG_PIXEL_GRAYSCALE:
memcpy(grayLine, pPixels, width); if (bitsPerSample == 8) {
memcpy(grayLine, pPixels, width);
} else {
for (int x = 0; x < width; x++) {
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
}
}
break; break;
case PNG_PIXEL_TRUECOLOR: case PNG_PIXEL_TRUECOLOR:
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
uint8_t* p = &pPixels[x * 3]; const uint8_t* p = &pPixels[x * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
} }
break; break;
@@ -125,7 +159,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
if (palette) { if (palette) {
if (hasAlpha) { if (hasAlpha) {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
uint8_t idx = pPixels[x]; uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
uint8_t* p = &palette[idx * 3]; uint8_t* p = &palette[idx * 3];
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = palette[768 + idx]; uint8_t alpha = palette[768 + idx];
@@ -133,12 +167,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
} }
} else { } else {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
uint8_t* p = &palette[pPixels[x] * 3]; uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
uint8_t* p = &palette[idx * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
} }
} }
} else { } else {
memcpy(grayLine, pPixels, width); for (int x = 0; x < width; x++) {
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
}
} }
break; break;
@@ -152,7 +189,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
case PNG_PIXEL_TRUECOLOR_ALPHA: case PNG_PIXEL_TRUECOLOR_ALPHA:
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
uint8_t* p = &pPixels[x * 4]; const uint8_t* p = &pPixels[x * 4];
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = p[3]; uint8_t alpha = p[3];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255); grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
@@ -172,74 +209,83 @@ int pngDrawCallback(PNGDRAW* pDraw) {
int srcY = pDraw->y; int srcY = pDraw->y;
int srcWidth = ctx->srcWidth; int srcWidth = ctx->srcWidth;
// Calculate destination Y with scaling // Map source rows with the exact output-height ratio. During downscaling,
int dstY = (int)(srcY * ctx->scale); // multiple source rows can select the same output row; during upscaling, one
// source row must be repeated across every output row in its range. Emitting
// only the first row of an upscale leaves zero-filled (black) gaps in the
// streamed pixel cache.
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
int endDstY = firstDstY + 1;
if (ctx->dstHeight > ctx->srcHeight) {
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
}
// Skip if we already rendered this destination row (multiple source rows map to same dest) if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
if (dstY == ctx->lastDstY) return 1; if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
ctx->lastDstY = dstY; if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
// Check bounds
if (dstY >= ctx->dstHeight) return 1;
int outY = ctx->config->y + dstY;
if (outY >= ctx->screenHeight) return 1;
// Convert entire source line to grayscale (improves cache locality) // Convert entire source line to grayscale (improves cache locality)
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette, convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
pDraw->iHasAlpha); pDraw->iHasAlpha);
// Render scaled row using Bresenham-style integer stepping (no floating-point division) // Render scaled rows using Bresenham-style integer stepping (no floating-point division)
int dstWidth = ctx->dstWidth; int dstWidth = ctx->dstWidth;
int outXBase = ctx->config->x; int outXBase = ctx->config->x;
int screenWidth = ctx->screenWidth; int screenWidth = ctx->screenWidth;
bool useDithering = ctx->config->useDithering; bool useDithering = ctx->config->useDithering;
bool caching = ctx->caching;
// Pre-compute orientation and render-mode state once per row // Pre-compute orientation and render-mode state once per callback.
DirectPixelWriter pw; DirectPixelWriter pw;
pw.init(*ctx->renderer); pw.init(*ctx->renderer);
pw.beginRow(outY);
// The cache streams to disk one row at a time. Flushing rows below this one for (int dstY = firstDstY; dstY < endDstY; dstY++) {
// (PNGdec delivers scanlines top to bottom) repositions the single-row band. ctx->lastDstY = dstY;
// A flush failure stops caching for the rest of the decode so we never write int outY = ctx->config->y + dstY;
// past the band buffer; finalize() then drops the partial file. if (outY >= ctx->screenHeight) continue;
DirectCacheWriter cw;
if (caching) {
if (!ctx->cache.advanceTo(dstY)) {
caching = false;
ctx->caching = false;
} else {
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
}
}
int srcX = 0; pw.beginRow(outY);
int error = 0;
for (int dstX = 0; dstX < dstWidth; dstX++) { // The cache streams to disk one row at a time. Flushing rows below this one
int outX = outXBase + dstX; // (PNGdec delivers scanlines top to bottom) repositions the single-row band.
if (outX < screenWidth) { // A flush failure stops caching for the rest of the decode so we never write
uint8_t gray = ctx->grayLineBuffer[srcX]; // past the band buffer; finalize() then drops the partial file.
bool caching = ctx->caching;
uint8_t ditheredGray; DirectCacheWriter cw;
if (useDithering) { if (caching) {
ditheredGray = applyBayerDither4Level(gray, outX, outY); if (!ctx->cache.advanceTo(dstY)) {
caching = false;
ctx->caching = false;
} else { } else {
ditheredGray = gray / 85; cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
if (ditheredGray > 3) ditheredGray = 3; cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
} }
pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
} }
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth int srcX = 0;
error += srcWidth; int error = 0;
while (error >= dstWidth) {
error -= dstWidth; for (int dstX = 0; dstX < dstWidth; dstX++) {
srcX++; int outX = outXBase + dstX;
if (outX < screenWidth) {
uint8_t gray = ctx->grayLineBuffer[srcX];
uint8_t ditheredGray;
if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
}
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
error += srcWidth;
while (error >= dstWidth) {
error -= dstWidth;
srcX++;
}
} }
} }
@@ -332,30 +378,44 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
} }
ctx.lastDstY = -1; // Reset row tracking ctx.lastDstY = -1; // Reset row tracking
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
ctx.scale, png->getBpp());
const int pixelType = png->getPixelType(); const int pixelType = png->getPixelType();
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType); const int bitsPerSample = png->getBpp();
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) { if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
LOG_ERR("PNG", LOG_ERR(
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d", "PNG",
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS); "PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS);
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow"); LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
return false; return false;
} }
if (png->getBpp() != 8) { if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath); warnUnsupportedFeature(
"bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath);
return false;
} }
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode // The converter expands each source row to 8-bit grayscale before dithering,
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2; // so this scratch buffer is sized by source pixels even when PNGdec reads a
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize)); // packed 1/2/4-bit row internally.
if (!ctx.grayLineBuffer) { constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2;
const size_t grayBufSize = static_cast<size_t>(ctx.srcWidth);
if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) {
LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast<unsigned>(grayBufSize),
ctx.srcWidth, static_cast<unsigned>(MAX_GRAY_LINE_BUFFER_BYTES));
return false;
}
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
if (!grayLineBuffer) {
LOG_ERR("PNG", "Failed to allocate gray line buffer"); LOG_ERR("PNG", "Failed to allocate gray line buffer");
return false; return false;
} }
ctx.grayLineBuffer = grayLineBuffer.get();
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to // Stream the pixel cache to disk. PNGdec delivers source scanlines top to
// bottom and we emit at most one (downscaled) output row per callback, so the // bottom and we emit at most one (downscaled) output row per callback, so the
@@ -375,7 +435,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
rc = png->decode(&ctx, 0); rc = png->decode(&ctx, 0);
unsigned long decodeTime = millis() - decodeStart; unsigned long decodeTime = millis() - decodeStart;
free(ctx.grayLineBuffer);
ctx.grayLineBuffer = nullptr; ctx.grayLineBuffer = nullptr;
if (rc != PNG_SUCCESS) { if (rc != PNG_SUCCESS) {
+3
View File
@@ -803,6 +803,9 @@ bool CssParser::loadFromCache() {
return false; return false;
} }
// Size the bucket array up front to avoid incremental rehashes while loading rules.
rulesBySelector_.reserve(ruleCount);
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool { auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
return static_cast<size_t>(file.available()) >= neededBytes; return static_cast<size_t>(file.available()) >= neededBytes;
}; };
+14
View File
@@ -67,6 +67,20 @@ class CssParser {
*/ */
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue); [[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
// Approximate resident heap of the parsed stylesheet, for the audit log.
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
size_t residentBytes() const {
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
for (const auto& kv : rulesBySelector_) {
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
total += sizeof(kv);
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
}
return total;
}
/** /**
* Check if any rules have been loaded * Check if any rules have been loaded
*/ */
+1 -1
View File
@@ -24,7 +24,7 @@ struct Iso639Mapping {
}; };
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"}, static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"}, {"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
{"ukr", "uk"}, {"swe", "sv"}}; {"ukr", "uk"}, {"swe", "sv"}, {"fin", "fi"}};
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator. // Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) { const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
@@ -7,6 +7,7 @@
#include "generated/hyph-de.trie.h" #include "generated/hyph-de.trie.h"
#include "generated/hyph-en.trie.h" #include "generated/hyph-en.trie.h"
#include "generated/hyph-es.trie.h" #include "generated/hyph-es.trie.h"
#include "generated/hyph-fi.trie.h"
#include "generated/hyph-fr.trie.h" #include "generated/hyph-fr.trie.h"
#include "generated/hyph-it.trie.h" #include "generated/hyph-it.trie.h"
#include "generated/hyph-pl.trie.h" #include "generated/hyph-pl.trie.h"
@@ -26,8 +27,9 @@ LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic); LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator finnishHyphenator(fi_patterns, isLatinLetter, toLowerLatin);
using EntryArray = std::array<LanguageEntry, 9>; using EntryArray = std::array<LanguageEntry, 10>;
const EntryArray& entries() { const EntryArray& entries() {
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator}, static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
@@ -38,7 +40,8 @@ const EntryArray& entries() {
{"italian", "it", &italianHyphenator}, {"italian", "it", &italianHyphenator},
{"polish", "pl", &polishHyphenator}, {"polish", "pl", &polishHyphenator},
{"swedish", "sv", &swedishHyphenator}, {"swedish", "sv", &swedishHyphenator},
{"ukrainian", "uk", &ukrainianHyphenator}}}; {"ukrainian", "uk", &ukrainianHyphenator},
{"finnish", "fi", &finnishHyphenator}}};
return kEntries; return kEntries;
} }
@@ -0,0 +1,95 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t fi_trie_data[] = {
0x01, 0x01, 0x16, 0x0B, 0x0C, 0x17, 0x0C, 0x15, 0x0C, 0x0B, 0x16, 0x29, 0x2B, 0x20, 0x1F, 0x0C,
0x33, 0x02, 0x0B, 0x02, 0x0B, 0x0C, 0x01, 0x0C, 0x34, 0x0B, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x20,
0x0B, 0x21, 0xA0, 0x00, 0x41, 0xA0, 0x02, 0x51, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00,
0x41, 0x62, 0xFD, 0xA0, 0x01, 0xA2, 0xA1, 0x00, 0x81, 0x6F, 0xFD, 0xA3, 0x00, 0x81, 0x69, 0x6F,
0x75, 0xF8, 0xF8, 0xF8, 0x28, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72, 0xDE, 0xDE, 0xEA,
0xDE, 0xDE, 0xDE, 0xF2, 0xF7, 0x22, 0xA4, 0xB6, 0xCD, 0xCD, 0xA1, 0x00, 0x81, 0x61, 0xD9, 0x28,
0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x72, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xF6, 0xFB,
0xA2, 0x00, 0x81, 0x61, 0x65, 0xC3, 0xC3, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72,
0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xE3, 0xFF, 0xF9,
0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92,
0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0xC5, 0xFF, 0xA6, 0xFF, 0xCA, 0x47, 0x61, 0x65, 0x69,
0x6F, 0x75, 0x79, 0xC3, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76,
0xFF, 0xA9, 0xA0, 0x00, 0xF1, 0x21, 0x73, 0xFD, 0xA1, 0x00, 0x41, 0x75, 0xFD, 0xA0, 0x00, 0x81,
0x43, 0x61, 0x65, 0x69, 0xFF, 0x63, 0xFF, 0x63, 0xFF, 0x63, 0xC1, 0x01, 0xA2, 0x61, 0xFF, 0x59,
0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0x76, 0xFF, 0x42, 0xFF, 0xE8, 0xFF,
0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x75, 0xFF, 0xED, 0xFF, 0xF0, 0xFF, 0xFA, 0xA1,
0x00, 0x41, 0x73, 0xCE, 0x47, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xFB, 0xFF, 0x1E,
0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x51, 0xA0, 0x01, 0x52, 0x21, 0x6F, 0xFD,
0x22, 0x74, 0x6E, 0xFD, 0xFD, 0xA0, 0x01, 0x73, 0x21, 0x6E, 0xFD, 0x22, 0x61, 0x6F, 0xFD, 0xFA,
0x21, 0x61, 0xEA, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xF2, 0xA4, 0x00, 0x41, 0x6E, 0x6A, 0x69, 0x6C,
0xE7, 0xF2, 0xFA, 0xFD, 0x21, 0x73, 0xE1, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41, 0x64, 0xFD, 0x21,
0x74, 0xD6, 0x21, 0x73, 0xFD, 0x22, 0x65, 0x69, 0xFA, 0xFD, 0x21, 0x61, 0xCB, 0x21, 0x6E, 0xBD,
0x22, 0x74, 0x6F, 0xBD, 0xFD, 0x21, 0x69, 0xC0, 0x21, 0x61, 0xFD, 0xA4, 0x00, 0x41, 0x70, 0x73,
0x74, 0x6D, 0xEA, 0xEF, 0xF5, 0xFD, 0x21, 0x69, 0xD9, 0xA1, 0x00, 0x41, 0x6C, 0xFD, 0x47, 0x61,
0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xBB, 0xFF, 0xCC, 0xFE, 0xA4, 0xFF, 0xED, 0xFE, 0xA4,
0xFF, 0xFB, 0xFE, 0xD7, 0xA0, 0x01, 0x41, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41,
0x72, 0xFD, 0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFE, 0x80, 0xFF, 0xFB,
0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0xB3, 0xFF, 0x2B, 0xFE, 0x94, 0x21, 0x61,
0xDC, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0xFF, 0x3E, 0xFE, 0x61, 0xFE, 0x61,
0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x94, 0xFF, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0x80, 0xFF,
0x40, 0x42, 0x6F, 0x65, 0xFF, 0x84, 0xFF, 0x47, 0x41, 0x75, 0xFF, 0x32, 0x21, 0x74, 0xFC, 0x42,
0x61, 0x6F, 0xFF, 0xFD, 0xFF, 0x36, 0xA4, 0x00, 0x41, 0x73, 0x6C, 0x6A, 0x70, 0xE4, 0xEB, 0xF9,
0xF2, 0x41, 0x79, 0xFF, 0x24, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x41, 0x73, 0xFD,
0x42, 0x2E, 0x6E, 0xFF, 0x15, 0xFF, 0x15, 0x21, 0x61, 0xF9, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x41,
0x64, 0xFD, 0x41, 0x65, 0xFE, 0xF8, 0x21, 0x6A, 0xFC, 0x42, 0x6B, 0x74, 0xFE, 0xFC, 0xFE, 0xFC,
0x21, 0x73, 0xF9, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0x41, 0x68, 0x70, 0x73, 0xFF, 0xF0, 0xFF, 0xFD,
0xFF, 0x24, 0x41, 0x74, 0xFF, 0x23, 0xC2, 0x00, 0x41, 0x72, 0x68, 0xFF, 0x30, 0xFF, 0xFC, 0xA0,
0x00, 0x52, 0x21, 0x72, 0xFD, 0x21, 0x69, 0xFA, 0x21, 0x6C, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x68,
0xFD, 0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0x70, 0x63, 0xFF, 0x95, 0xFF, 0xAA,
0xFF, 0xBC, 0xFF, 0xD5, 0xFD, 0xC1, 0xFF, 0xE5, 0xFD, 0xF4, 0xFF, 0xF1, 0xFF, 0xF7, 0xFF, 0xFD,
0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x73, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD,
0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xD5, 0xFF, 0xDE, 0xA0, 0x00, 0x92, 0xA0, 0x00, 0xB2, 0xA0,
0x01, 0x01, 0xC3, 0x00, 0x61, 0x69, 0x65, 0x79, 0xFE, 0x20, 0xFE, 0x20, 0xFF, 0xFD, 0x22, 0xA4,
0xB6, 0xF4, 0xAD, 0x25, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0xA8, 0xE6, 0xE6, 0xE9, 0xFB, 0x42, 0xB6,
0xA4, 0xFF, 0x9D, 0xFF, 0x9D, 0x46, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0x65, 0xFF, 0x96, 0xFF, 0xD4,
0xFF, 0xD4, 0xFF, 0xD7, 0xFF, 0xF9, 0xFF, 0xD7, 0x22, 0xA4, 0xB6, 0xDB, 0xED, 0xA0, 0x00, 0x72,
0xA0, 0x00, 0x71, 0x21, 0xA4, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xA4, 0xFD, 0x21, 0x69, 0xF4, 0xA0,
0x01, 0x22, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x26, 0x61, 0x6F, 0x75, 0xC3,
0x65, 0x6C, 0xE2, 0xE2, 0xE2, 0xEE, 0xF1, 0xFD, 0x22, 0xA4, 0xB6, 0xD8, 0xD8, 0x21, 0x61, 0xD3,
0xA0, 0x00, 0xB1, 0x24, 0x75, 0x69, 0x65, 0x6F, 0xCD, 0xCD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F,
0x75, 0xF4, 0xF4, 0xF4, 0xF4, 0x25, 0x79, 0xC3, 0x61, 0x75, 0x69, 0xBB, 0xE3, 0xE8, 0xEE, 0xF7,
0xA0, 0x00, 0xD2, 0x22, 0xA4, 0xB6, 0xFD, 0xFD, 0x44, 0x61, 0x65, 0x6F, 0x69, 0xFF, 0x64, 0xFF,
0x64, 0xFF, 0x64, 0xFF, 0x64, 0x42, 0x65, 0x61, 0xFF, 0x9B, 0xFF, 0xCB, 0x21, 0x65, 0xC4, 0x22,
0x61, 0x75, 0xC1, 0xC1, 0xA0, 0x02, 0x32, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x49, 0x79, 0xC3,
0x75, 0x61, 0x65, 0x69, 0x6F, 0x73, 0x6C, 0xFF, 0x80, 0xFF, 0xD6, 0xFF, 0xDB, 0xFF, 0xB0, 0xFF,
0xE8, 0xFF, 0xEF, 0xFF, 0xF2, 0xFD, 0x70, 0xFF, 0xFD, 0x44, 0x69, 0x65, 0x6F, 0x75, 0xFF, 0x23,
0xFF, 0x23, 0xFF, 0x23, 0xFF, 0x23, 0x43, 0x75, 0x61, 0x65, 0xFF, 0x5A, 0xFF, 0x8A, 0xFF, 0x8A,
0x41, 0x76, 0xFF, 0x5F, 0x21, 0x61, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD,
0x21, 0x65, 0xFD, 0x43, 0x69, 0x6F, 0x6B, 0xFF, 0xF1, 0xFD, 0xF7, 0xFF, 0xFD, 0xA0, 0x01, 0xA4,
0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x43, 0x6E, 0x74, 0x6B, 0xFC, 0x7D, 0xFC, 0x7D, 0xFF, 0xFD,
0x41, 0x69, 0xFC, 0x73, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x48, 0xC3, 0x61, 0x75,
0x65, 0x6F, 0x69, 0x6C, 0x73, 0xFF, 0x3C, 0xFF, 0xAD, 0xFF, 0xBA, 0xFF, 0x20, 0xFF, 0x20, 0xFF,
0x50, 0xFF, 0xD7, 0xFF, 0xFD, 0x44, 0x61, 0x69, 0x75, 0x79, 0xFE, 0xB7, 0xFE, 0xB7, 0xFE, 0xB7,
0xFE, 0xB7, 0x42, 0x61, 0x69, 0xFE, 0xEE, 0xFE, 0xEE, 0x42, 0x75, 0x61, 0xFE, 0xE7, 0xFF, 0x17,
0x42, 0xA4, 0xB6, 0xFE, 0xE6, 0xFF, 0x30, 0x24, 0x65, 0x61, 0x75, 0xC3, 0xDE, 0xEB, 0xF2, 0xF9,
0x43, 0x61, 0x65, 0x6F, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0x42, 0x61, 0x75, 0xFE, 0xC6, 0xFE,
0xC6, 0x44, 0x75, 0x61, 0x65, 0x6F, 0xFE, 0xBF, 0xFE, 0xEF, 0xFE, 0xEF, 0xFE, 0xEF, 0x41, 0xB6,
0xFE, 0xB2, 0x21, 0xC3, 0xFC, 0x42, 0xA4, 0xB6, 0xFE, 0xB1, 0xFF, 0xFD, 0x43, 0x61, 0x6F, 0x79,
0xFE, 0xD4, 0xFE, 0xD4, 0xFE, 0xD4, 0x42, 0x61, 0x65, 0xFE, 0x56, 0xFE, 0x56, 0x26, 0x69, 0x61,
0x75, 0xC3, 0x65, 0x6F, 0xC3, 0xCD, 0xD4, 0xE8, 0xEF, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0xA4, 0xFD,
0xA0, 0x01, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x02, 0x03, 0x21,
0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x75, 0xFD, 0x23, 0xC3, 0x79, 0x73, 0xE2,
0xEE, 0xFD, 0x41, 0x72, 0xFD, 0xD9, 0x42, 0x6C, 0x68, 0xFC, 0x47, 0xFF, 0xFC, 0xC1, 0x00, 0x81,
0x69, 0xFB, 0xA6, 0x21, 0x76, 0xFA, 0x59, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0xC3, 0x79, 0x6F, 0x75, 0x61, 0x65, 0x69, 0x2E, 0x63, 0x71,
0xFB, 0xAE, 0xFB, 0xC9, 0xFB, 0xE1, 0xFB, 0xFA, 0xFC, 0x16, 0xFC, 0x16, 0xFC, 0x4A, 0xFC, 0x6E,
0xFC, 0x16, 0xFC, 0xE8, 0xFD, 0x0C, 0xFD, 0x2B, 0xFD, 0xCB, 0xFD, 0xEA, 0xFC, 0x16, 0xFE, 0x42,
0xFE, 0x65, 0xFE, 0x8F, 0xFE, 0xC7, 0xFF, 0x36, 0xFF, 0x71, 0xFF, 0xB7, 0xFF, 0xE5, 0xFF, 0xF0,
0xFF, 0xFD,
};
constexpr SerializedHyphenationPatterns fi_patterns = {
0x496u,
fi_trie_data,
sizeof(fi_trie_data),
};
+41 -22
View File
@@ -35,7 +35,7 @@ constexpr const char* BOLD_TAGS[] = {"b", "strong"};
constexpr const char* ITALIC_TAGS[] = {"i", "em"}; constexpr const char* ITALIC_TAGS[] = {"i", "em"};
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"}; constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"}; constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
constexpr const char* IMAGE_TAGS[] = {"img"}; constexpr const char* IMAGE_TAGS[] = {"img", "image"};
constexpr const char* SKIP_TAGS[] = {"head"}; constexpr const char* SKIP_TAGS[] = {"head"};
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; } bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
@@ -225,6 +225,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues); currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
partWordBufferIndex = 0; partWordBufferIndex = 0;
nextWordContinues = false; nextWordContinues = false;
listItemBulletOnly = false;
} }
// start a new text block if needed // start a new text block if needed
@@ -254,6 +255,17 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
return; return;
} }
// <li> added a bullet as the first word, making the block non-empty. When a nested
// block-level child (<p>, <div>, etc.) opens, reuse the block instead of flushing
// the bullet to its own line. The bullet stays inline with the child's text.
if (listItemBulletOnly) {
const auto style = currentTextBlock->getBlockStyle();
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
listItemBulletOnly = false;
flushPendingAnchor();
return;
}
makePages(); makePages();
} }
// If the pending anchor is a TOC chapter boundary, force a page break after the previous // If the pending anchor is a TOC chapter boundary, force a page break after the previous
@@ -261,6 +273,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
flushPendingAnchor(); flushPendingAnchor();
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle)); currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
wordsExtractedInBlock = 0; wordsExtractedInBlock = 0;
listItemBulletOnly = false;
} }
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) { void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
@@ -495,11 +508,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
for (int i = 0; atts[i]; i += 2) { for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "src") == 0) { if (strcmp(atts[i], "src") == 0) {
src = atts[i + 1]; src = atts[i + 1];
} else if (src.empty() && (strcmp(atts[i], "href") == 0 || strcmp(atts[i], "xlink:href") == 0)) {
src = atts[i + 1];
} else if (strcmp(atts[i], "alt") == 0) { } else if (strcmp(atts[i], "alt") == 0) {
alt = atts[i + 1]; alt = atts[i + 1];
} }
} }
const size_t fragmentPos = src.find('#');
if (fragmentPos != std::string::npos) {
src.resize(fragmentPos);
}
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely // imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
if (self->imageRendering == 2) { if (self->imageRendering == 2) {
self->skipUntilDepth = self->depth; self->skipUntilDepth = self->depth;
@@ -507,19 +527,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return; return;
} }
// Skip image if CSS display:none
if (self->cssParser) {
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
if (!styleAttr.empty()) {
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
}
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
}
if (!src.empty() && self->imageRendering != 1) { if (!src.empty() && self->imageRendering != 1) {
LOG_DBG("EHP", "Found image: src=%s", src.c_str()); LOG_DBG("EHP", "Found image: src=%s", src.c_str());
@@ -543,24 +550,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096); extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
cachedImageFile.flush(); cachedImageFile.flush();
cachedImageFile.close(); cachedImageFile.close();
delay(50); // Give SD card time to sync
} }
if (extractSuccess) { if (extractSuccess) {
// Get image dimensions // Get image dimensions, retrying to absorb SD-card sync latency on slow
// cards. Replaces a blanket delay(50) that cost ~50ms on every image, and
// closes the silent-drop bug where a single getDimensions failure was fatal.
ImageDimensions dims = {0, 0}; ImageDimensions dims = {0, 0};
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath); ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
if (decoder && decoder->getDimensions(cachedImagePath, dims)) { bool gotDimensions = false;
for (int attempt = 0; attempt < 3 && !gotDimensions; attempt++) {
if (attempt > 0) {
delay(50); // Give a slow SD card time to finish syncing before retrying
}
gotDimensions = decoder && decoder->getDimensions(cachedImagePath, dims);
}
if (gotDimensions) {
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height); LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
int displayWidth = 0; int displayWidth = 0;
int displayHeight = 0; int displayHeight = 0;
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId)); const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{}; const CssStyle& imgStyle = cssStyle;
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
if (!styleAttr.empty()) {
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
}
const bool hasCssHeight = imgStyle.hasImageHeight(); const bool hasCssHeight = imgStyle.hasImageHeight();
const bool hasCssWidth = imgStyle.hasImageWidth(); const bool hasCssWidth = imgStyle.hasImageWidth();
@@ -882,6 +893,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (strcmp(name, "li") == 0) { if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR); self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
self->listItemBulletOnly = true;
} }
} }
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) { } else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
@@ -1288,6 +1300,13 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
} }
self->blockStyleStack.pop_back(); self->blockStyleStack.pop_back();
} }
// </li> closes: if the bullet never got inline text (empty <li> or <li> with only
// block children that were flushed), clear the flag so the next sibling doesn't
// merge into this block.
if (strcmp(name, "li") == 0) {
self->listItemBulletOnly = false;
}
} }
} }
@@ -80,6 +80,7 @@ class ChapterHtmlSlimParser {
int tableDepth = 0; int tableDepth = 0;
int tableRowIndex = 0; int tableRowIndex = 0;
int tableColIndex = 0; int tableColIndex = 0;
bool listItemBulletOnly = false; // true when currentTextBlock has only the <li> bullet
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on // Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0; int completedPageCount = 0;
+6 -5
View File
@@ -137,8 +137,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error."); LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
} }
// Sort item index for binary search if we have enough items // Sort the (unconditionally-built) item index so every idref lookup uses binary
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) { // search. Without this, small/medium manifests fell back to an O(spine × manifest)
// linear rescan of .items.bin per itemref (up to ~200ms/item at large scale).
if (!self->itemIndex.empty()) {
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) { std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen); return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
}); });
@@ -284,9 +286,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
++it; ++it;
} }
} else { } else {
// Slow path: linear scan (for small manifests, keeps original behavior) // Fallback linear scan, only reached when the index is empty (no manifest
// TODO: This lookup is slow as need to scan through all items each time. // items). The fast binary-search path above is used for all real manifests.
// It can take up to 200ms per item when getting to 1500 items.
self->tempItemStore.seek(0); self->tempItemStore.seek(0);
std::string itemId; std::string itemId;
while (self->tempItemStore.available()) { while (self->tempItemStore.available()) {
+1 -3
View File
@@ -32,7 +32,7 @@ class ContentOpfParser final : public Print {
HalFile tempItemStore; HalFile tempItemStore;
std::string coverItemId; std::string coverItemId;
// Index for fast idref→href lookup (used only for large EPUBs) // Index for fast idref→href lookup (binary search over .items.bin)
struct ItemIndexEntry { struct ItemIndexEntry {
uint32_t idHash; // FNV-1a hash of itemId uint32_t idHash; // FNV-1a hash of itemId
uint16_t idLen; // length for collision reduction uint16_t idLen; // length for collision reduction
@@ -41,8 +41,6 @@ class ContentOpfParser final : public Print {
std::deque<ItemIndexEntry> itemIndex; std::deque<ItemIndexEntry> itemIndex;
bool useItemIndex = false; bool useItemIndex = false;
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
// FNV-1a hash function // FNV-1a hash function
static uint32_t fnvHash(const std::string& s) { static uint32_t fnvHash(const std::string& s) {
uint32_t hash = 2166136261u; uint32_t hash = 2166136261u;
+14 -8
View File
@@ -62,7 +62,14 @@ void FontCacheManager::resetStats() {
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; } bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) { void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
scanText_ += text; if (!text) return;
const size_t remaining = (scanTextLen_ < SCAN_TEXT_CAPACITY - 1) ? (SCAN_TEXT_CAPACITY - 1 - scanTextLen_) : 0;
if (remaining > 0) {
const size_t textLen = strnlen(text, remaining);
memcpy(scanText_ + scanTextLen_, text, textLen);
scanTextLen_ += textLen;
scanText_[scanTextLen_] = '\0';
}
if (scanFontId_ < 0) scanFontId_ = fontId; if (scanFontId_ < 0) scanFontId_ = fontId;
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03; const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
const unsigned char* p = reinterpret_cast<const unsigned char*>(text); const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
@@ -80,15 +87,15 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
manager_->scanMode_ = ScanMode::Scanning; manager_->scanMode_ = ScanMode::Scanning;
manager_->clearCache(); manager_->clearCache();
manager_->resetStats(); manager_->resetStats();
manager_->scanText_.clear(); manager_->scanTextLen_ = 0;
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat manager_->scanText_[0] = '\0';
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_)); memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
manager_->scanFontId_ = -1; manager_->scanFontId_ = -1;
} }
void FontCacheManager::PrewarmScope::endScanAndPrewarm() { void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
manager_->scanMode_ = ScanMode::None; manager_->scanMode_ = ScanMode::None;
if (manager_->scanText_.empty()) return; if (manager_->scanTextLen_ == 0) return;
// Build style bitmask from all styles that appeared during the scan // Build style bitmask from all styles that appeared during the scan
uint8_t styleMask = 0; uint8_t styleMask = 0;
@@ -97,11 +104,10 @@ void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
} }
if (styleMask == 0) styleMask = 1; // default to regular if (styleMask == 0) styleMask = 1; // default to regular
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask); manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_, styleMask);
// Free scan string memory manager_->scanTextLen_ = 0;
manager_->scanText_.clear(); manager_->scanText_[0] = '\0';
manager_->scanText_.shrink_to_fit();
} }
FontCacheManager::PrewarmScope::~PrewarmScope() { FontCacheManager::PrewarmScope::~PrewarmScope() {
+4 -2
View File
@@ -2,9 +2,9 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <string>
class FontDecompressor; class FontDecompressor;
class SdCardFont; class SdCardFont;
@@ -51,7 +51,9 @@ class FontCacheManager {
enum class ScanMode : uint8_t { None, Scanning }; enum class ScanMode : uint8_t { None, Scanning };
ScanMode scanMode_ = ScanMode::None; ScanMode scanMode_ = ScanMode::None;
std::string scanText_; static constexpr size_t SCAN_TEXT_CAPACITY = 2048;
char scanText_[SCAN_TEXT_CAPACITY] = {};
size_t scanTextLen_ = 0;
uint32_t scanStyleCounts_[4] = {}; uint32_t scanStyleCounts_[4] = {};
int scanFontId_ = -1; int scanFontId_ = -1;
}; };
+137 -21
View File
@@ -1,6 +1,7 @@
#include "GfxRenderer.h" #include "GfxRenderer.h"
#include <BidiUtils.h> #include <BidiUtils.h>
#include <BuildScratch.h>
#include <FontDecompressor.h> #include <FontDecompressor.h>
#include <HalGPIO.h> #include <HalGPIO.h>
#include <Logging.h> #include <Logging.h>
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
namespace { namespace {
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir); const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
// Appends the shaped visual form of every RTL token in `text` to `shapedOut`.
// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream,
// so the SD advance table must be warmed with the presentation forms as well as
// the logical codepoints — otherwise every RTL word measurement misses the fast
// path and falls through to onGlyphMiss(), which opens the .cpfont and reads
// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph.
// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so
// pure-LTR text pays almost nothing.
void appendShapedRtlTokens(const char* text, std::string& shapedOut) {
const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; };
std::string token;
std::string visual;
const char* p = text;
while (*p) {
while (*p && isBreak(*p)) ++p;
const char* start = p;
bool hasRtlBytes = false;
while (*p && !isBreak(*p)) {
const auto b = static_cast<unsigned char>(*p);
hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB);
++p;
}
if (!hasRtlBytes) continue;
token.assign(start, p - start);
if (BidiUtils::applyBidiVisual(token.c_str(), visual, static_cast<int>(BidiUtils::BidiBaseDir::AUTO))) {
shapedOut += visual;
}
}
}
} // namespace } // namespace
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const { const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
@@ -57,7 +88,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const { void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
auto it = sdCardFonts_.find(fontId); auto it = sdCardFonts_.find(fontId);
if (it != sdCardFonts_.end()) { if (it != sdCardFonts_.end()) {
int missed = it->second->buildAdvanceTable(utf8Text, styleMask); std::string shaped;
appendShapedRtlTokens(utf8Text, shaped);
int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str());
if (missed > 0) { if (missed > 0) {
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed); LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
} }
@@ -71,7 +104,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::strin
// Augment the persistent advance-only table for layout measurement. // Augment the persistent advance-only table for layout measurement.
// The table survives across paragraphs/sections (capped per font), so // The table survives across paragraphs/sections (capped per font), so
// repeated indexing of the same SD font amortizes glyph-metric SD reads. // repeated indexing of the same SD font amortizes glyph-metric SD reads.
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask); std::string shaped;
for (const auto& w : words) {
appendShapedRtlTokens(w.c_str(), shaped);
}
int missed =
it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str());
if (missed > 0) { if (missed > 0) {
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed); LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
} }
@@ -91,6 +129,47 @@ void GfxRenderer::begin() {
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr); bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
} }
void GfxRenderer::releaseFrameBufferForBuild() {
// Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so
// it cannot move and repeated loans cannot fragment the heap (the previous
// free+realloc model measurably decayed the max contiguous block over a
// session). The bytes are deposited in the build-scratch registry so
// memory-hungry build phases (e.g. InflateStream's tinfl state + window)
// can claim them instead of allocating.
uint32_t size = 0;
uint8_t* scratch = display.lendFrameBufferStorage(&size);
frameBuffer = nullptr;
if (scratch) {
buildscratch::lend(scratch, size);
}
}
bool GfxRenderer::restoreFrameBufferAfterBuild() {
buildscratch::reclaim();
display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed
frameBuffer = display.getFrameBuffer();
return frameBuffer != nullptr;
}
GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) {
// Nesting guard: if the framebuffer is already lent out (an outer loan),
// stay inert so this end() cannot return storage the outer loan still owns.
if (!renderer_.hasFrameBuffer()) return;
renderer_.releaseFrameBufferForBuild();
active_ = true;
}
void GfxRenderer::FrameBufferLoan::end() {
if (!active_) return;
active_ = false;
if (!renderer_.restoreFrameBufferAfterBuild()) {
// Only reachable if the framebuffer never existed, which begin() already
// asserts against; kept as a backstop since running blind helps nobody.
LOG_ERR("GFX", "Framebuffer restore failed - restarting");
ESP.restart();
}
}
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); } bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
@@ -418,18 +497,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
uint32_t cp; uint32_t cp;
uint32_t prevCp = 0; uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
// Skip Hebrew Niqqud (vowel marks) // RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported. // path: zero-advance overlays on the preceding base glyph (applyBidiVisual
if (cp >= 0x0591 && cp <= 0x05C7) { // emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
continue; // niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
} // marks stay centered, raised above the base or (kasra) at their
// font-native position. Fonts without their glyphs — the built-ins — miss
if (utf8IsCombiningMark(cp)) { // the getGlyph lookup and skip them, as before.
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
if (!combiningGlyph) continue; if (!combiningGlyph) continue;
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); const auto anchor = combiningMark::anchorFor(cp);
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left, const int raiseBy =
combiningGlyph->width); combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
combiningGlyph->left, combiningGlyph->width);
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style); renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
continue; continue;
} }
@@ -1369,6 +1451,20 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
display.displayBuffer(refreshMode, fadingFix); display.displayBuffer(refreshMode, fadingFix);
} }
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
// The async path has no turn-off-screen hook, which the sunlight fading fix
// relies on; keep those users on the blocking path.
if (fadingFix) {
display.displayBuffer(refreshMode, fadingFix);
return;
}
display.displayBufferAsync(refreshMode);
}
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
if (!text || maxWidth <= 0) return ""; if (!text || maxWidth <= 0) return "";
@@ -1615,6 +1711,15 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
} }
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const { int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
// Measure the exact codepoint stream drawText renders: bidi-reordered and
// Arabic-shaped (contextual presentation forms, Lam-Alef collapse).
// Measuring the raw logical text counts the Alef a ligature absorbs and
// uses base-letter advances instead of presentation-form advances, so RTL
// lines come out wider than they draw — uneven word gaps and a ragged
// right margin.
std::string visual;
text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO);
// Advance table fast-path for SD card fonts during layout. // Advance table fast-path for SD card fonts during layout.
// No kerning/ligature lookup — consistent with previous metadataOnly behavior // No kerning/ligature lookup — consistent with previous metadataOnly behavior
// where kern/lig data was not loaded. // where kern/lig data was not loaded.
@@ -1630,6 +1735,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
} }
const auto& font = fontIt->second; const auto& font = fontIt->second;
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) { while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
if (BidiUtils::isTransparentMark(cp)) {
continue;
}
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx); int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
if (advFP == 0 && !utf8IsCombiningMark(cp)) { if (advFP == 0 && !utf8IsCombiningMark(cp)) {
const EpdGlyph* glyph = font.getGlyph(cp, style); const EpdGlyph* glyph = font.getGlyph(cp, style);
@@ -1652,6 +1761,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
const auto& font = fontIt->second; const auto& font = fontIt->second;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
if (BidiUtils::isTransparentMark(cp)) {
continue;
}
if (utf8IsCombiningMark(cp)) { if (utf8IsCombiningMark(cp)) {
continue; continue;
} }
@@ -1728,18 +1841,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
uint32_t cp; uint32_t cp;
uint32_t prevCp = 0; uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
// Skip Hebrew Niqqud (vowel marks) // RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported. // path: zero-advance overlays on the preceding base glyph (applyBidiVisual
if (cp >= 0x0591 && cp <= 0x05C7) { // emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
continue; // niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
} // marks stay centered, raised above the base or (kasra) at their
// font-native position. Fonts without their glyphs — the built-ins — miss
if (utf8IsCombiningMark(cp)) { // the getGlyph lookup and skip them, as before.
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
if (!combiningGlyph) continue; if (!combiningGlyph) continue;
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); const auto anchor = combiningMark::anchorFor(cp);
const int raiseBy =
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
const int combiningX = x - raiseBy; const int combiningX = x - raiseBy;
const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth, const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
combiningGlyph->left, combiningGlyph->width); combiningGlyph->left, combiningGlyph->width);
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style); renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
continue; continue;
+39
View File
@@ -135,6 +135,17 @@ class GfxRenderer {
int getScreenWidth() const; int getScreenWidth() const;
int getScreenHeight() const; int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
// grayscale strip rendering) can overlap the panel's refresh time. The
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
// support. See HalDisplay::displayBufferAsync for the baseline contract.
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
void waitRefreshComplete() const;
// True when displayBufferAsync() genuinely overlaps: panel defers and
// fadingFix isn't forcing the blocking path. Callers can skip overlap
// scaffolding (e.g. whole-plane grayscale buffers) when false.
bool supportsAsyncRefresh() const;
// EXPERIMENTAL: Windowed update - display only a rectangular region // EXPERIMENTAL: Windowed update - display only a rectangular region
// void displayWindow(int x, int y, int width, int height) const; // void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;
@@ -250,6 +261,34 @@ class GfxRenderer {
// Font helpers // Font helpers
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const; const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
// builds) WITHOUT freeing the allocation, so it never moves and repeated
// loans cannot fragment the heap. Between release and restore NOTHING may
// draw or display — the panel keeps showing its last refreshed image. The
// lent bytes are published via buildscratch::claim() for consumers like
// InflateStream. restore returns the buffer white, so the caller must
// redraw the full screen; it cannot fail (no allocation involved).
void releaseFrameBufferForBuild();
bool restoreFrameBufferAfterBuild();
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
// RAII form of the loan above, for blocking build regions with early-return
// error paths: restores on scope exit (or explicitly via end()). Display the
// popup/screen the panel should hold BEFORE constructing one. Constructing
// while the framebuffer is already lent yields an inert loan (nesting-safe).
class FrameBufferLoan {
public:
explicit FrameBufferLoan(GfxRenderer& renderer);
~FrameBufferLoan() { end(); }
void end();
FrameBufferLoan(const FrameBufferLoan&) = delete;
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
private:
GfxRenderer& renderer_;
bool active_ = false;
};
// Low level functions // Low level functions
uint8_t* getFrameBuffer() const; uint8_t* getFrameBuffer() const;
size_t getBufferSize() const; size_t getBufferSize() const;
+3
View File
@@ -87,6 +87,7 @@ STR_USERNAME: "Імя карыстальніка"
STR_PASSWORD: "Пароль" STR_PASSWORD: "Пароль"
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі" STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў" STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
STR_SEND_METADATA: "Адпраўляць метаданыя дакумента"
STR_AUTHENTICATE: "Аўтарызацыя" STR_AUTHENTICATE: "Аўтарызацыя"
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader" STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
STR_KOREADER_PASSWORD: "Пароль KOReader" STR_KOREADER_PASSWORD: "Пароль KOReader"
@@ -300,3 +301,5 @@ STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: " STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)" STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам" STR_TILT_PAGE_TURN: "Перагортванне нахілам"
STR_ADD_HIDDEN_NETWORK: "Дадаць схаваную сетку..."
STR_ENTER_WIFI_SSID: "Увядзіце назву сеткі (SSID)"
+64 -55
View File
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
STR_SLEEPING: "ENTRANT EN REPÒS" STR_SLEEPING: "ENTRANT EN REPÒS"
STR_ENTERING_SLEEP: "Entrant en repòs" STR_ENTERING_SLEEP: "Entrant en repòs"
STR_BROWSE_FILES: "Explora fitxers" STR_BROWSE_FILES: "Explora fitxers"
STR_FILE_TRANSFER: "Transferència" STR_FILE_TRANSFER: "Transferència de fitxers"
STR_SETTINGS_TITLE: "Configuració" STR_SETTINGS_TITLE: "Configuració"
STR_CONTINUE_READING: "Continua llegint" STR_CONTINUE_READING: "Continua llegint"
STR_NO_OPEN_BOOK: "Cap llibre obert" STR_NO_OPEN_BOOK: "Cap llibre obert"
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
STR_END_OF_BOOK: "Final del llibre" STR_END_OF_BOOK: "Final del llibre"
STR_EMPTY_CHAPTER: "Capítol buit" STR_EMPTY_CHAPTER: "Capítol buit"
STR_INDEXING: "S'està indexant" STR_INDEXING: "S'està indexant"
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
STR_MEMORY_ERROR: "Error de memòria" STR_MEMORY_ERROR: "Error de memòria"
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina" STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
STR_EMPTY_FILE: "Fitxer buit" STR_EMPTY_FILE: "Fitxer buit"
@@ -28,58 +29,63 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
STR_NO_NETWORKS: "No s'han trobat xarxes" STR_NO_NETWORKS: "No s'han trobat xarxes"
STR_NETWORKS_FOUND: "%zu xarxes trobades" STR_NETWORKS_FOUND: "%zu xarxes trobades"
STR_SCANNING: "S'està escanejant..." STR_SCANNING: "S'està escanejant..."
STR_FINDING_SAVED_WIFI: "S'estan cercant xarxes Wi-Fi desades..."
STR_CONNECTING: "S'està connectant..." STR_CONNECTING: "S'està connectant..."
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi desada..."
STR_SHOW_NETWORKS: "Mostra"
STR_CONNECTED: "S'ha connectat!" STR_CONNECTED: "S'ha connectat!"
STR_CONNECTION_FAILED: "Error de connexió" STR_CONNECTION_FAILED: "Error de connexió"
STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?" STR_FORGET_NETWORK: "Vols oblidar aquesta xarxa?"
STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?" STR_SAVE_PASSWORD: "Vols desar la contrasenya per a la propera vegada?"
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar" STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
STR_JOIN_NETWORK: "Uneix-te a una xarxa" STR_JOIN_NETWORK: "Uneix-te a una xarxa"
STR_CREATE_HOTSPOT: "Crea un punt d'accés" STR_CREATE_HOTSPOT: "Crea un punt d'accés"
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent" STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi" STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..." STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
STR_HOTSPOT_MODE: "Mode de punt d'accés" STR_HOTSPOT_MODE: "Mode de punt d'accés"
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a aquesta xarxa Wi-Fi" STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a aquesta xarxa Wi-Fi"
STR_OPEN_URL_HINT: "Obriu aquest URL al navegador" STR_OPEN_URL_HINT: "Obre aquest URL al navegador"
STR_OR_HTTP_PREFIX: "o http://" STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:" STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
STR_CALIBRE_WIRELESS: "Calibre sense fils" STR_CALIBRE_WIRELESS: "Calibre sense fils"
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat" STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
STR_MAC_ADDRESS: "Adreça MAC:" STR_MAC_ADDRESS: "Adreça MAC:"
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..." STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi" STR_ENTER_WIFI_PASSWORD: "Introdueix la contrasenya Wi-Fi"
STR_TO_PREFIX: "a " STR_TO_PREFIX: "a "
STR_CALIBRE_RECEIVING: "S'està rebent: " STR_CALIBRE_RECEIVING: "S'està rebent: "
STR_CALIBRE_RECEIVED: "S'ha rebut: " STR_CALIBRE_RECEIVED: "S'ha rebut: "
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi" STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\"" STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu aquesta pantalla oberta mentre s'envia\"" STR_CALIBRE_INSTRUCTION_4: "\"Mantén aquesta pantalla oberta mentre s'envia\""
STR_CAT_DISPLAY: "Visualització" STR_CAT_DISPLAY: "Visualització"
STR_CAT_READER: "Lector" STR_CAT_READER: "Lector"
STR_CAT_CONTROLS: "Controls" STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "Sistema" STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Pantalla de repòs" STR_SLEEP_SCREEN: "Pantalla de repòs"
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs" STR_SLEEP_COVER_MODE: "Ajust de la portada en repòs"
STR_HIDE_BATTERY: "Oculta el % de bateria" STR_HIDE_BATTERY: "Oculta el % de bateria"
STR_EXTRA_SPACING: "Espaiat de paràgraf extra" STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
STR_TEXT_AA: "Antialiàsing del text" STR_TEXT_AA: "Antialiàsing del text"
STR_IMAGES: "Imatges" STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_PLACEHOLDER: "Text alternatiu"
STR_IMAGES_SUPPRESS: "Suprimir" STR_IMAGES_SUPPRESS: "Suprimir"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada" STR_EOB_HOME: "Inici"
STR_EOB_CONTINUE_WITH: "Continua amb"
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura" STR_ORIENTATION: "Orientació de lectura"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó" STR_LONG_PRESS_BEHAVIOR: "Acció en mantenir premut un botó"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
STR_LONG_PRESS_MENU: "Funció de pulsació llarga" STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
STR_FONT_FAMILY: "Tipus de lletra" STR_FONT_FAMILY: "Font del lector"
STR_FONT_SIZE: "Mida de la lletra (UI)" STR_FONT_SIZE: "Cos de lletra del lector"
STR_LINE_SPACING: "Interlineat del lector" STR_LINE_SPACING: "Interlineat del lector"
STR_SCREEN_MARGIN: "Marge de pantalla del lector" STR_SCREEN_MARGIN: "Marge de pantalla del lector"
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector" STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
@@ -88,15 +94,16 @@ STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
STR_SHOW_HIDDEN_FILES: "Mostra fitxers ocults" STR_SHOW_HIDDEN_FILES: "Mostra fitxers ocults"
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents" STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read" STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
STR_REFRESH_FREQ: "Freqüència de refresc" STR_REFRESH_FREQ: "Freqüència d'actualització"
STR_KOREADER_SYNC: "Sincronització del KOReader" STR_KOREADER_SYNC: "Sincronització del KOReader"
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions" STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
STR_LANGUAGE: "Idioma" STR_LANGUAGE: "Llengua"
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura" STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
STR_USERNAME: "Nom d'usuari" STR_USERNAME: "Nom d'usuari"
STR_PASSWORD: "Contrasenya" STR_PASSWORD: "Contrasenya"
STR_SYNC_SERVER_URL: "URL del servidor de sincronització" STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
STR_DOCUMENT_MATCHING: "Coincidència de documents" STR_DOCUMENT_MATCHING: "Coincidència de documents"
STR_SEND_METADATA: "Envia metadades del document"
STR_AUTHENTICATE: "Autentica" STR_AUTHENTICATE: "Autentica"
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader" STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
STR_KOREADER_PASSWORD: "Contrasenya del KOReader" STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
@@ -142,7 +149,7 @@ STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
STR_KOSYNC: "KOSync" STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Punt de llibre" STR_BOOKMARK_OPTION: "Punt de llibre"
STR_DISABLED: "Desactivats" STR_DISABLED: "Sense funció"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Petita" STR_SMALL: "Petita"
@@ -170,16 +177,16 @@ STR_UPDATING: "S'està actualitzant..."
STR_NO_UPDATE: "No hi ha actualitzacions disponibles" STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
STR_UPDATE_FAILED: "Ha fallat l'actualització" STR_UPDATE_FAILED: "Ha fallat l'actualització"
STR_UPDATE_COMPLETE: "Actualització completada" STR_UPDATE_COMPLETE: "Actualització completada"
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar" STR_POWER_ON_HINT: "Prem i mantén premut el botó d'encesa per tornar a engegar"
STR_NO_ENTRIES: "No s'ha trobat cap entrada" STR_NO_ENTRIES: "No s'ha trobat cap entrada"
STR_DOWNLOADING: "S'està baixant..." STR_DOWNLOADING: "S'està baixant..."
STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Sense nom" STR_UNNAMED: "Sense nom"
STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar" STR_HOLD_OPEN_TO_DELETE: "Mantén premut Obre per esborrar"
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor" STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed" STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
STR_NETWORK_PREFIX: "Xarxa: " STR_NETWORK_PREFIX: "Xarxa: "
STR_IP_ADDRESS_PREFIX: "Adreça IP: " STR_IP_ADDRESS_PREFIX: "Adreça IP: "
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general" STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
@@ -192,7 +199,7 @@ STR_HOME: "« Inici"
STR_SELECT: "Selecciona" STR_SELECT: "Selecciona"
STR_SELECTED: "Seleccionat" STR_SELECTED: "Seleccionat"
STR_TOGGLE: "Canvia" STR_TOGGLE: "Canvia"
STR_TOGGLE_BOOKMARK: "Commuta punt de llibre" STR_TOGGLE_BOOKMARK: "Afegeix o elimina el punt de llibre"
STR_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
STR_CONNECT: "Connecta" STR_CONNECT: "Connecta"
@@ -211,7 +218,7 @@ STR_DIR_RIGHT: "Dreta"
STR_DIR_UP: "Amunt" STR_DIR_UP: "Amunt"
STR_DIR_DOWN: "Avall" STR_DIR_DOWN: "Avall"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs" STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
STR_FILTER_CONTRAST: "Contrast" STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat" STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol" STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
@@ -233,7 +240,7 @@ STR_THEME_CLASSIC: "Clàssic"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat" STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol" STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps" STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_BOOKMARKS: "Punts de llibre" STR_BOOKMARKS: "Punts de llibre"
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre." STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
@@ -242,17 +249,17 @@ STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida" STR_QUICK_RESUME: "Represa ràpida"
STR_MENU_RECENT_BOOKS: "Llibres recents" STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Voleu suprimir-lo de Llibres recents?" STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents" STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre" STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
STR_FORGET_AND_REMOVE: "Voleu suprimir la contrasenya desada?" STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i suprimir la contrasenya desada?"
STR_FORGET_BUTTON: "Oblida" STR_FORGET_BUTTON: "Oblida"
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..." STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
STR_CALIBRE_SETUP: "Configura" STR_CALIBRE_SETUP: "Configura"
STR_CALIBRE_STATUS: "Estat" STR_CALIBRE_STATUS: "Estat"
STR_CLEAR_BUTTON: "Esborra" STR_CLEAR_BUTTON: "Esborra"
STR_DEFAULT_VALUE: "Per defecte" STR_DEFAULT_VALUE: "Per defecte"
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol" STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
STR_UNASSIGNED: "No assignat" STR_UNASSIGNED: "No assignat"
STR_ALREADY_ASSIGNED: "Ja assignat" STR_ALREADY_ASSIGNED: "Ja assignat"
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte" STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
@@ -266,19 +273,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
STR_SYNC_PROGRESS: "Sincronitza el progrés" STR_SYNC_PROGRESS: "Sincronitza el progrés"
STR_DELETE_CACHE: "Esborra la memòria cau del llibre" STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
STR_DELETE: "Esborra" STR_DELETE: "Esborra"
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar aquest punt de llibre?" STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar aquest punt de llibre?"
STR_DISPLAY_QR: "Mostra la pàgina com a QR" STR_DISPLAY_QR: "Mostra la pàgina com a QR"
STR_CHAPTER_PREFIX: "Capítol: " STR_CHAPTER_PREFIX: "Capítol: "
STR_PAGES_SEPARATOR: " pàgines | " STR_PAGES_SEPARATOR: " pàgines | "
STR_BOOK_PREFIX: "Llibre: " STR_BOOK_PREFIX: "Llibre: "
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL" STR_CALIBRE_URL_HINT: "Per al Calibre, afegeix /opds a la URL"
STR_SYNCING_TIME: "S'està sincronitzant el temps..." STR_SYNCING_TIME: "S'està sincronitzant el temps..."
STR_CALC_HASH: "S'està calculant el hash del document..." STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document" STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..." STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..." STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials" STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader a la configuració" STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader a la configuració"
STR_PROGRESS_FOUND: "S'ha trobat progrés!" STR_PROGRESS_FOUND: "S'ha trobat progrés!"
STR_REMOTE_LABEL: "Remot:" STR_REMOTE_LABEL: "Remot:"
STR_LOCAL_LABEL: "Local:" STR_LOCAL_LABEL: "Local:"
@@ -288,7 +295,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
STR_APPLY_REMOTE: "Aplica el progrés remot" STR_APPLY_REMOTE: "Aplica el progrés remot"
STR_UPLOAD_LOCAL: "Puja el progrés local" STR_UPLOAD_LOCAL: "Puja el progrés local"
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot" STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?" STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
STR_UPLOAD_SUCCESS: "Progrés pujat!" STR_UPLOAD_SUCCESS: "Progrés pujat!"
STR_SYNC_FAILED_MSG: "Sincronització fallida" STR_SYNC_FAILED_MSG: "Sincronització fallida"
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés" STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
@@ -307,11 +314,11 @@ STR_SLEEP_NEVER: "Mai"
STR_STEP_HINT_FRONT: "Botons frontals:" STR_STEP_HINT_FRONT: "Botons frontals:"
STR_STEP_HINT_SIDE: "Botons laterals:" STR_STEP_HINT_SIDE: "Botons laterals:"
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla" STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: " STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació" STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
STR_FORCE_REFRESH: "Refresca la pantalla" STR_FORCE_REFRESH: "Refresca la pantalla"
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons." STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantén premut el botó d'encesa durant uns segons."
STR_NEXT_PAGE: "Pàgina següent »" STR_NEXT_PAGE: "Pàgina següent »"
STR_PREV_PAGE: "« Pàgina anterior" STR_PREV_PAGE: "« Pàgina anterior"
STR_XTC_STATUS_BAR: "Barra d'estat XTC" STR_XTC_STATUS_BAR: "Barra d'estat XTC"
@@ -340,20 +347,20 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats" STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Suprimeix el servidor" STR_DELETE_SERVER: "Suprimeix el servidor"
STR_OPDS_SERVERS: "Servidors OPDS" STR_OPDS_SERVERS: "Servidors OPDS"
STR_MANAGE_FONTS: "Gestiona els tipus de lletra" STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de tipus de lletra" STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..." STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles" STR_NO_FONTS_AVAILABLE: "No hi ha fonts disponibles"
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!" STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
STR_FONT_INSTALLED: "Tipus de lletra instal·lat!" STR_FONT_INSTALLED: "Font instal·lada!"
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra" STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació de la font"
STR_INSTALLED: "Instal·lat" STR_INSTALLED: "Instal·lat"
STR_DOWNLOAD_ALL: "Descarrega-ho tot" STR_DOWNLOAD_ALL: "Descarrega-ho tot"
STR_UPDATE_ALL: "Actualitza-ho tot" STR_UPDATE_ALL: "Actualitza-ho tot"
STR_UPDATE_AVAILABLE: "Actualitza" STR_UPDATE_AVAILABLE: "Actualitza"
STR_CRASH_TITLE: "Bloqueig del sistema" STR_CRASH_TITLE: "Fallada del sistema"
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Incloeu aquest fitxer a l'informe d'errors." STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Inclou aquest fitxer a l'informe d'errors."
STR_CRASH_REASON: "Motiu del bloqueig:" STR_CRASH_REASON: "Motiu de la fallada"
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)" STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor" STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor" STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
@@ -366,20 +373,22 @@ STR_KB_TIPS: "Consells:"
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat" STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL" STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL"
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text" STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari" STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT: caràcter secundari"
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari" STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT: majúscules o caràcter secundari"
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari" STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT: minúscules o caràcter secundari"
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments" STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD" STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un fitxer de firmware (.bin)" STR_SELECT_FIRMWARE_FILE: "Selecciona un fitxer de firmware (.bin)"
STR_NO_BIN_FILES: "No s'han trobat fitxers .bin" STR_NO_BIN_FILES: "No s'han trobat fitxers .bin"
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..." STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid" STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició" STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit" STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?" STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer" STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware" STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apaguis el dispositiu!"
STR_RECOVERY_MODE: "Mode de recuperació" STR_RECOVERY_MODE: "Mode de recuperació"
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo" STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
STR_ADD_HIDDEN_NETWORK: "Afegeix una xarxa oculta..."
STR_ENTER_WIFI_SSID: "Introdueix el nom de la xarxa (SSID)"
+4 -1
View File
@@ -49,7 +49,7 @@ STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
STR_MAC_ADDRESS: "MAC adresa:" STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola Wi-Fi..." STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi" STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
STR_TO_PREFIX: "pro" STR_TO_PREFIX: "k "
STR_CALIBRE_RECEIVING: "Příjem:" STR_CALIBRE_RECEIVING: "Příjem:"
STR_CALIBRE_RECEIVED: "Přijato:" STR_CALIBRE_RECEIVED: "Přijato:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
@@ -92,6 +92,7 @@ STR_USERNAME: "Uživatelské jméno"
STR_PASSWORD: "Heslo" STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synch. serveru" STR_SYNC_SERVER_URL: "URL synch. serveru"
STR_DOCUMENT_MATCHING: "Párování dokumentů" STR_DOCUMENT_MATCHING: "Párování dokumentů"
STR_SEND_METADATA: "Odesílat metadata dokumentu"
STR_AUTHENTICATE: "Ověření" STR_AUTHENTICATE: "Ověření"
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu" STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
STR_KOREADER_PASSWORD: "Heslo KOReaderu" STR_KOREADER_PASSWORD: "Heslo KOReaderu"
@@ -275,3 +276,5 @@ STR_SLEEP_NEVER: "Nikdy"
STR_STEP_HINT_FRONT: "Přední tlačítka:" STR_STEP_HINT_FRONT: "Přední tlačítka:"
STR_STEP_HINT_SIDE: "Boční tlačítka:" STR_STEP_HINT_SIDE: "Boční tlačítka:"
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním" STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
STR_ADD_HIDDEN_NETWORK: "Přidat skrytou síť..."
STR_ENTER_WIFI_SSID: "Zadejte název sítě (SSID)"
+3
View File
@@ -97,6 +97,7 @@ STR_USERNAME: "Brugernavn"
STR_PASSWORD: "Adgangskode" STR_PASSWORD: "Adgangskode"
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL" STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling" STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
STR_SEND_METADATA: "Send dokumentmetadata"
STR_AUTHENTICATE: "Godkend" STR_AUTHENTICATE: "Godkend"
STR_KOREADER_USERNAME: "KOReader brugernavn" STR_KOREADER_USERNAME: "KOReader brugernavn"
STR_KOREADER_PASSWORD: "KOReader adgangskode" STR_KOREADER_PASSWORD: "KOReader adgangskode"
@@ -303,3 +304,5 @@ STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: " STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
STR_TILT_PAGE_TURN: "Vip for at vende side" STR_TILT_PAGE_TURN: "Vip for at vende side"
STR_ADD_HIDDEN_NETWORK: "Tilføj skjult netværk..."
STR_ENTER_WIFI_SSID: "Indtast netværksnavn (SSID)"
+3
View File
@@ -97,6 +97,7 @@ STR_USERNAME: "Gebruikersnaam"
STR_PASSWORD: "Wachtwoord" STR_PASSWORD: "Wachtwoord"
STR_SYNC_SERVER_URL: "Sync-server URL" STR_SYNC_SERVER_URL: "Sync-server URL"
STR_DOCUMENT_MATCHING: "Documentkoppeling" STR_DOCUMENT_MATCHING: "Documentkoppeling"
STR_SEND_METADATA: "Documentmetadata versturen"
STR_AUTHENTICATE: "Authenticatie" STR_AUTHENTICATE: "Authenticatie"
STR_KOREADER_USERNAME: "KOReader gebruikersnaam" STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
STR_KOREADER_PASSWORD: "KOReader wachtwoord" STR_KOREADER_PASSWORD: "KOReader wachtwoord"
@@ -303,3 +304,5 @@ STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: " STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)" STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
STR_TILT_PAGE_TURN: "Kantel om te bladeren" STR_TILT_PAGE_TURN: "Kantel om te bladeren"
STR_ADD_HIDDEN_NETWORK: "Verborgen netwerk toevoegen..."
STR_ENTER_WIFI_SSID: "Voer netwerknaam in (SSID)"
+26
View File
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "No chapters"
STR_END_OF_BOOK: "End of book" STR_END_OF_BOOK: "End of book"
STR_EMPTY_CHAPTER: "Empty chapter" STR_EMPTY_CHAPTER: "Empty chapter"
STR_INDEXING: "Indexing" STR_INDEXING: "Indexing"
STR_INDEX_FAILED: "Failed to index - invalid book"
STR_MEMORY_ERROR: "Memory error" STR_MEMORY_ERROR: "Memory error"
STR_PAGE_LOAD_ERROR: "Page load error" STR_PAGE_LOAD_ERROR: "Page load error"
STR_EMPTY_FILE: "Empty file" STR_EMPTY_FILE: "Empty file"
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Wi-Fi Networks"
STR_NO_NETWORKS: "No networks found" STR_NO_NETWORKS: "No networks found"
STR_NETWORKS_FOUND: "%zu networks found" STR_NETWORKS_FOUND: "%zu networks found"
STR_SCANNING: "Scanning..." STR_SCANNING: "Scanning..."
STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..."
STR_CONNECTING: "Connecting..." STR_CONNECTING: "Connecting..."
STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..."
STR_SHOW_NETWORKS: "Show"
STR_CONNECTED: "Connected!" STR_CONNECTED: "Connected!"
STR_CONNECTION_FAILED: "Connection Failed" STR_CONNECTION_FAILED: "Connection Failed"
STR_FORGET_NETWORK: "Forget Network?" STR_FORGET_NETWORK: "Forget Network?"
@@ -49,6 +53,8 @@ STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
STR_MAC_ADDRESS: "MAC address:" STR_MAC_ADDRESS: "MAC address:"
STR_CHECKING_WIFI: "Checking Wi-Fi..." STR_CHECKING_WIFI: "Checking Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password" STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
STR_ADD_HIDDEN_NETWORK: "Add hidden network..."
STR_ENTER_WIFI_SSID: "Enter network name (SSID)"
STR_TO_PREFIX: "to " STR_TO_PREFIX: "to "
STR_CALIBRE_RECEIVING: "Receiving: " STR_CALIBRE_RECEIVING: "Receiving: "
STR_CALIBRE_RECEIVED: "Received: " STR_CALIBRE_RECEIVED: "Received: "
@@ -101,6 +107,7 @@ STR_USERNAME: "Username"
STR_PASSWORD: "Password" STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL" STR_SYNC_SERVER_URL: "Sync Server URL"
STR_DOCUMENT_MATCHING: "Document Matching" STR_DOCUMENT_MATCHING: "Document Matching"
STR_SEND_METADATA: "Send Document Metadata"
STR_AUTHENTICATE: "Authenticate" STR_AUTHENTICATE: "Authenticate"
STR_KOREADER_USERNAME: "KOReader Username" STR_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password" STR_KOREADER_PASSWORD: "KOReader Password"
@@ -288,6 +295,25 @@ STR_HW_BACK_LABEL: "Back (1st button)"
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)" STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
STR_HW_LEFT_LABEL: "Left (3rd button)" STR_HW_LEFT_LABEL: "Left (3rd button)"
STR_HW_RIGHT_LABEL: "Right (4th button)" STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_BLUETOOTH: "Bluetooth"
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
STR_BT_SCAN_PAIR: "Scan & Pair"
STR_BT_NO_DEVICES: "No devices found"
STR_BT_FREE_HINT1: "Set Free2/3 to Reader Mode and"
STR_BT_FREE_HINT2: "Volume Function to pair"
STR_BT_DISCONNECT: "Disconnect"
STR_BT_CONNECTED_TO: "Connected: %s"
STR_BT_NOT_CONNECTED: "Not connected"
STR_BT_PAIRED_DEVICES: "Paired Devices"
STR_BT_NO_PAIRED: "No paired devices"
STR_BT_MAP_BUTTONS: "Map Remote Buttons"
STR_BT_PRESS_REMOTE: "Press a button on your remote"
STR_BT_CONNECTING_POPUP: "BT Connecting..."
STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)"
STR_STATE_PAUSED: "PAUSED"
STR_BT_PAGE_FORWARD: "Page Forward"
STR_BT_PAGE_BACK: "Page Back"
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
STR_GO_TO_PERCENT: "Go to %" STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home" STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress" STR_SYNC_PROGRESS: "Sync Progress"
+3
View File
@@ -92,6 +92,7 @@ STR_USERNAME: "Käyttäjänimi"
STR_PASSWORD: "Salasana" STR_PASSWORD: "Salasana"
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite" STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus" STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
STR_SEND_METADATA: "Lähetä asiakirjan metatiedot"
STR_AUTHENTICATE: "Tunnistaudu" STR_AUTHENTICATE: "Tunnistaudu"
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi" STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
STR_KOREADER_PASSWORD: "KOReader-salasana" STR_KOREADER_PASSWORD: "KOReader-salasana"
@@ -273,3 +274,5 @@ STR_SLEEP_NEVER: "Ei koskaan"
STR_STEP_HINT_FRONT: "Etupainikkeet:" STR_STEP_HINT_FRONT: "Etupainikkeet:"
STR_STEP_HINT_SIDE: "Sivupainikkeet:" STR_STEP_HINT_SIDE: "Sivupainikkeet:"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla" STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
STR_ADD_HIDDEN_NETWORK: "Lisää piilotettu verkko..."
STR_ENTER_WIFI_SSID: "Anna verkon nimi (SSID)"
+3
View File
@@ -97,6 +97,7 @@ STR_USERNAME: "Nom dutilisateur"
STR_PASSWORD: "Mot de passe" STR_PASSWORD: "Mot de passe"
STR_SYNC_SERVER_URL: "URL du serveur" STR_SYNC_SERVER_URL: "URL du serveur"
STR_DOCUMENT_MATCHING: "Correspondance" STR_DOCUMENT_MATCHING: "Correspondance"
STR_SEND_METADATA: "Envoyer métadonnées"
STR_AUTHENTICATE: "Connexion" STR_AUTHENTICATE: "Connexion"
STR_KOREADER_USERNAME: "Utilisateur" STR_KOREADER_USERNAME: "Utilisateur"
STR_KOREADER_PASSWORD: "Mot de passe" STR_KOREADER_PASSWORD: "Mot de passe"
@@ -304,3 +305,5 @@ STR_SCREENSHOT_BUTTON: "Capture d'écran"
STR_AUTO_TURN_ENABLED: "Tourne-page auto : " STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
STR_TILT_PAGE_TURN: "Tourner par inclinaison" STR_TILT_PAGE_TURN: "Tourner par inclinaison"
STR_ADD_HIDDEN_NETWORK: "Ajouter un réseau masqué..."
STR_ENTER_WIFI_SSID: "Saisir le nom du réseau (SSID)"
+3
View File
@@ -91,6 +91,7 @@ STR_USERNAME: "Benutzername"
STR_PASSWORD: "Passwort" STR_PASSWORD: "Passwort"
STR_SYNC_SERVER_URL: "Sync-Server-URL" STR_SYNC_SERVER_URL: "Sync-Server-URL"
STR_DOCUMENT_MATCHING: "Dateizuordnung" STR_DOCUMENT_MATCHING: "Dateizuordnung"
STR_SEND_METADATA: "Metadaten senden"
STR_AUTHENTICATE: "Authentifizieren" STR_AUTHENTICATE: "Authentifizieren"
STR_KOREADER_USERNAME: "KOReader-Benutzername" STR_KOREADER_USERNAME: "KOReader-Benutzername"
STR_KOREADER_PASSWORD: "KOReader-Passwort" STR_KOREADER_PASSWORD: "KOReader-Passwort"
@@ -380,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Schreiben der Firmware-Datei ist fehlgeschlagen"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
STR_RECOVERY_MODE: "Wiederherstellungsmodus" STR_RECOVERY_MODE: "Wiederherstellungsmodus"
STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus" STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus"
STR_ADD_HIDDEN_NETWORK: "Verstecktes Netzwerk hinzufügen..."
STR_ENTER_WIFI_SSID: "Netzwerknamen eingeben (SSID)"
+3
View File
@@ -95,6 +95,7 @@ STR_USERNAME: "שם משתמש"
STR_PASSWORD: "סיסמה" STR_PASSWORD: "סיסמה"
STR_SYNC_SERVER_URL: "כתובת שרת סנכרון" STR_SYNC_SERVER_URL: "כתובת שרת סנכרון"
STR_DOCUMENT_MATCHING: "התאמת מסמכים" STR_DOCUMENT_MATCHING: "התאמת מסמכים"
STR_SEND_METADATA: "שלח מטא-נתוני מסמך"
STR_AUTHENTICATE: "התחבר" STR_AUTHENTICATE: "התחבר"
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader" STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
STR_KOREADER_PASSWORD: "סיסמת KOReader" STR_KOREADER_PASSWORD: "סיסמת KOReader"
@@ -391,3 +392,5 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
STR_SLEEP_NEVER: "אף פעם" STR_SLEEP_NEVER: "אף פעם"
STR_STEP_HINT_FRONT: "לחצנים קדמיים:" STR_STEP_HINT_FRONT: "לחצנים קדמיים:"
STR_STEP_HINT_SIDE: "לחצני צד:" STR_STEP_HINT_SIDE: "לחצני צד:"
STR_ADD_HIDDEN_NETWORK: "הוסף רשת מוסתרת..."
STR_ENTER_WIFI_SSID: "הזן שם רשת (SSID)"
+93
View File
@@ -49,6 +49,8 @@ STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
STR_MAC_ADDRESS: "MAC-cím:" STR_MAC_ADDRESS: "MAC-cím:"
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..." STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót" STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
STR_ADD_HIDDEN_NETWORK: "Rejtett hálózat hozzáadása..."
STR_ENTER_WIFI_SSID: "Add meg a hálózat nevét (SSID)"
STR_TO_PREFIX: "- " STR_TO_PREFIX: "- "
STR_CALIBRE_RECEIVING: "Fogadás: " STR_CALIBRE_RECEIVING: "Fogadás: "
STR_CALIBRE_RECEIVED: "Fogadva: " STR_CALIBRE_RECEIVED: "Fogadva: "
@@ -94,6 +96,7 @@ STR_USERNAME: "Felhasználónév"
STR_PASSWORD: "Jelszó" STR_PASSWORD: "Jelszó"
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL" STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés" STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
STR_SEND_METADATA: "Dokumentum metaadatok küldése"
STR_AUTHENTICATE: "Hitelesítés" STR_AUTHENTICATE: "Hitelesítés"
STR_KOREADER_USERNAME: "KOReader felhasználónév" STR_KOREADER_USERNAME: "KOReader felhasználónév"
STR_KOREADER_PASSWORD: "KOReader jelszó" STR_KOREADER_PASSWORD: "KOReader jelszó"
@@ -300,3 +303,93 @@ STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: " STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
STR_TILT_PAGE_TURN: "Döntéses lapozás" STR_TILT_PAGE_TURN: "Döntéses lapozás"
STR_ADD_SERVER: "Szerver hozzáadása"
STR_BOOKMARKS: "Könyvjelzők"
STR_BOOKMARK_ADDED: "Könyvjelző hozzáadva."
STR_BOOKMARK_OPTION: "Könyvjelző"
STR_BOTTOM: "Alul"
STR_CLOCK: "Óra"
STR_CLOCK_FORMAT: "Óraformátum"
STR_CLOCK_FORMAT_12H: "12 órás"
STR_CLOCK_FORMAT_24H: "24 órás"
STR_CLOCK_SYNC: "Óra szinkronizálása"
STR_CLOCK_SYNCED: "Óra szinkronizálva"
STR_CLOCK_SYNCING: "Szinkronizálás NTP-ről..."
STR_CLOCK_SYNC_FAIL: "Szinkronizálás sikertelen"
STR_CLOCK_SYNC_NOW: "Óra szinkronizálása most"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nincs csatlakoztatva"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Először csatlakozz Wi-Fi-hez, majd próbáld újra."
STR_CLOCK_SYNC_OK: "Óra szinkronizálva"
STR_CLOCK_UTC_OFFSET: "Óra UTC eltolás"
STR_CONFIRM_DELETE_BOOKMARK: "Törlöd ezt a könyvjelzőt?"
STR_CONNECTING_SAVED_WIFI: "Csatlakozás mentett Wi-Fi-hez..."
STR_CRASH_DESCRIPTION: "A részletes jelentés a crash_report.txt fájlba lett mentve. Kérjük, csatold ezt a fájlt a hibajelentéshez."
STR_CRASH_NO_REASON: "(Nem került rögzítésre ok)"
STR_CRASH_REASON: "Összeomlás oka:"
STR_CRASH_TITLE: "Rendszerösszeomlás"
STR_CURRENT_TIME: "Aktuális idő:"
STR_DELETE_SERVER: "Szerver törlése"
STR_DISABLED: "Letiltva"
STR_DOWNLOAD_ALL: "Összes letöltése"
STR_EOB_CONTINUE_WITH: "Folytatás ezzel"
STR_EOB_HOME: "Kezdőképernyő"
STR_FINDING_SAVED_WIFI: "Mentett Wi-Fi keresése..."
STR_FIRMWARE_FILE_OPEN_FAILED: "A fájl nem nyitható meg"
STR_FIRMWARE_TOO_LARGE: "A firmware túl nagy a partícióhoz"
STR_FIRMWARE_TOO_SMALL: "A firmware fájl túl kicsi"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne kapcsold ki!"
STR_FIRMWARE_UPDATE_PROMPT: "Frissíted a firmware-t?"
STR_FIRMWARE_WRITE_FAILED: "A firmware írása sikertelen"
STR_FONT_BROWSER: "Betűtípus-böngésző"
STR_FONT_INSTALLED: "Betűtípus telepítve!"
STR_FONT_INSTALL_FAILED: "A betűtípus telepítése sikertelen"
STR_FORCE_REFRESH: "Képernyő frissítése"
STR_INDEX_FAILED: "Indexelés sikertelen érvénytelen könyv"
STR_INSTALLED: "Telepítve"
STR_INVALID_FIRMWARE: "Érvénytelen firmware fájl"
STR_KB_HINT_CLEAR_TEXT: "Tartsd nyomva a DEL-t az összes szöveg törléséhez"
STR_KB_HINT_EDIT_ENTRY: "Tartsd nyomva a FEL-t a bejegyzés szerkesztéséhez"
STR_KB_HINT_EXIT_URL_MODE: "Nyomd meg az ABC-t az URL mód elhagyásához"
STR_KB_HINT_HIDE_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg a [***]-ot a jelszó elrejtéséhez"
STR_KB_HINT_LOWER_SECONDARY: "Tartsd nyomva a SELECT-et kisbetűért vagy másodlagos karakterért"
STR_KB_HINT_MOVE_CURSOR: "Nyomd meg a BALRA vagy JOBBRA gombot a kurzor mozgatásához"
STR_KB_HINT_RETURN_CURSOR: "Nyomd meg a BALRA-t a kurzorpozícióhoz való visszatéréshez"
STR_KB_HINT_RETURN_KEYBOARD: "Nyomd meg a LE-t a billentyűzethez való visszatéréshez"
STR_KB_HINT_SECONDARY_CHAR: "Tartsd nyomva a SELECT-et másodlagos karakterért"
STR_KB_HINT_SHOW_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg az [abc]-t a jelszó megjelenítéséhez"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Nyomd meg a [***]-ot a jelszó elrejtéséhez"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Nyomd meg az [abc]-t a jelszó megjelenítéséhez"
STR_KB_HINT_UPPER_SECONDARY: "Tartsd nyomva a SELECT-et NAGYBETŰÉRT vagy másodlagos karakterért"
STR_KB_HINT_URL_SNIPPETS: "Nyomd meg az URL-t az URL-részletekért"
STR_KB_TIPS: "Tippek:"
STR_KOSYNC: "KOSync"
STR_LOADING_FONT_LIST: "Betűtípuslista betöltése..."
STR_LONG_PRESS_BEHAVIOR: "Hosszú gombnyomás viselkedése"
STR_LONG_PRESS_BEHAVIOR_OFF: "KI"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Tájolásváltás"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Fejezetugrás"
STR_LONG_PRESS_MENU: "Hosszú nyomás Menü"
STR_MANAGE_FONTS: "Betűtípusok kezelése"
STR_NEXT_FIELD: "Következő"
STR_NEXT_PAGE: "Következő oldal »"
STR_NO_BIN_FILES: "Nincs .bin fájl"
STR_NO_FONTS_AVAILABLE: "Nincs elérhető betűtípus"
STR_NO_SERVERS: "Nincs beállított OPDS szerver"
STR_OPDS_SERVERS: "OPDS szerverek"
STR_PREV_PAGE: "« Előző oldal"
STR_PWR_BTN_FOOTNOTE_BACK: "Gyors visszatérés lábjegyzetből"
STR_RECOVERY_MODE: "Helyreállítási mód"
STR_RECOVERY_MODE_HINT: "Helyezd a firmware.bin fájlt az SD-kártya gyökerébe, majd válaszd ki"
STR_RESTARTING_HINT: "Újraindítás... Ha az eszköz nem indul újra, tartsd nyomva a bekapcsológombot néhány másodpercig."
STR_SD_FIRMWARE_UPDATE: "Firmware frissítés SD-kártyáról"
STR_SEARCH: "Keresés"
STR_SELECT_FIRMWARE_FILE: "Válassz firmware fájlt (.bin)"
STR_SERVER_NAME: "Szerver neve"
STR_SET_SLEEP_COVER: "Borító beállítása"
STR_SHOW_NETWORKS: "Megjelenítés"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_TOP: "Felül"
STR_UPDATE_ALL: "Összes frissítése"
STR_UPDATE_AVAILABLE: "Frissítés"
STR_VALIDATING_FIRMWARE: "Firmware ellenőrzése..."
STR_XTC_STATUS_BAR: "XTC állapotsáv"
+6
View File
@@ -98,6 +98,7 @@ STR_USERNAME: "Nome utente"
STR_PASSWORD: "Password" STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "URL server di sincronizzazione" STR_SYNC_SERVER_URL: "URL server di sincronizzazione"
STR_DOCUMENT_MATCHING: "Corrispondenza documenti" STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
STR_SEND_METADATA: "Invia metadati documento"
STR_AUTHENTICATE: "Autentica" STR_AUTHENTICATE: "Autentica"
STR_KOREADER_USERNAME: "Nome utente KOReader" STR_KOREADER_USERNAME: "Nome utente KOReader"
STR_KOREADER_PASSWORD: "Password KOReader" STR_KOREADER_PASSWORD: "Password KOReader"
@@ -383,3 +384,8 @@ STR_DISABLED: "Disattivato"
STR_BOOKMARK_OPTION: "Segnalibro" STR_BOOKMARK_OPTION: "Segnalibro"
STR_KOSYNC: "KOSync" STR_KOSYNC: "KOSync"
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note" STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
STR_EOB_CONTINUE_WITH: "Continua con"
STR_EOB_HOME: "Home"
STR_INDEX_FAILED: "Indicizzazione fallita - libro non valido"
STR_ADD_HIDDEN_NETWORK: "Aggiungi rete nascosta..."
STR_ENTER_WIFI_SSID: "Inserisci il nome della rete (SSID)"
+3
View File
@@ -88,6 +88,7 @@ STR_USERNAME: "Пайдаланушы аты"
STR_PASSWORD: "Құпия сөз" STR_PASSWORD: "Құпия сөз"
STR_SYNC_SERVER_URL: "Синхрондау сервері URL" STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру" STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
STR_SEND_METADATA: "Құжат метадеректерін жіберу"
STR_AUTHENTICATE: "Аутентификация" STR_AUTHENTICATE: "Аутентификация"
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты" STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
STR_KOREADER_PASSWORD: "KOReader құпия сөзі" STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
@@ -299,3 +300,5 @@ STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: " STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару" STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару"
STR_ADD_HIDDEN_NETWORK: "Жасырын желіні қосу..."
STR_ENTER_WIFI_SSID: "Желі атауын енгізіңіз (SSID)"
+3
View File
@@ -94,6 +94,7 @@ STR_USERNAME: "Vartotojas"
STR_PASSWORD: "Slaptažodis" STR_PASSWORD: "Slaptažodis"
STR_SYNC_SERVER_URL: "Serverio URL" STR_SYNC_SERVER_URL: "Serverio URL"
STR_DOCUMENT_MATCHING: "Atpažinimas" STR_DOCUMENT_MATCHING: "Atpažinimas"
STR_SEND_METADATA: "Siųsti dokumento metaduomenis"
STR_AUTHENTICATE: "Prisijungti" STR_AUTHENTICATE: "Prisijungti"
STR_KOREADER_USERNAME: "KOReader vartotojas" STR_KOREADER_USERNAME: "KOReader vartotojas"
STR_KOREADER_PASSWORD: "KOReader slaptažodis" STR_KOREADER_PASSWORD: "KOReader slaptažodis"
@@ -300,3 +301,5 @@ STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
STR_AUTO_TURN_ENABLED: "Auto-vertimas: " STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant" STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
STR_ADD_HIDDEN_NETWORK: "Pridėti paslėptą tinklą..."
STR_ENTER_WIFI_SSID: "Įveskite tinklo pavadinimą (SSID)"
+3
View File
@@ -97,6 +97,7 @@ STR_USERNAME: "Użytkownik"
STR_PASSWORD: "Hasło" STR_PASSWORD: "Hasło"
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji" STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów" STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
STR_SEND_METADATA: "Wyślij metadane dokumentu"
STR_AUTHENTICATE: "Uwierzytelnianie" STR_AUTHENTICATE: "Uwierzytelnianie"
STR_KOREADER_USERNAME: "Użytkownik KOReader" STR_KOREADER_USERNAME: "Użytkownik KOReader"
STR_KOREADER_PASSWORD: "Hasło KOReader" STR_KOREADER_PASSWORD: "Hasło KOReader"
@@ -360,3 +361,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
STR_RECOVERY_MODE: "Tryb przywracania" STR_RECOVERY_MODE: "Tryb przywracania"
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go" STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
STR_ADD_HIDDEN_NETWORK: "Dodaj ukrytą sieć..."
STR_ENTER_WIFI_SSID: "Wprowadź nazwę sieci (SSID)"
@@ -99,6 +99,7 @@ STR_USERNAME: "Nome de usuário"
STR_PASSWORD: "Senha" STR_PASSWORD: "Senha"
STR_SYNC_SERVER_URL: "URL servidor sincronização" STR_SYNC_SERVER_URL: "URL servidor sincronização"
STR_DOCUMENT_MATCHING: "Documento correspondente" STR_DOCUMENT_MATCHING: "Documento correspondente"
STR_SEND_METADATA: "Enviar metadados do doc."
STR_AUTHENTICATE: "Autenticar" STR_AUTHENTICATE: "Autenticar"
STR_KOREADER_USERNAME: "Usuário do KOReader" STR_KOREADER_USERNAME: "Usuário do KOReader"
STR_KOREADER_PASSWORD: "Senha do KOReader" STR_KOREADER_PASSWORD: "Senha do KOReader"
@@ -383,3 +384,5 @@ STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
STR_RECOVERY_MODE: "Modo de Recuperação" STR_RECOVERY_MODE: "Modo de Recuperação"
STR_RECOVERY_MODE_HINT: "Coloque o arquivo firmware.bin na raiz do cartão SD e selecione-o" STR_RECOVERY_MODE_HINT: "Coloque o arquivo firmware.bin na raiz do cartão SD e selecione-o"
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
STR_ENTER_WIFI_SSID: "Insira o nome da rede (SSID)"
+394
View File
@@ -0,0 +1,394 @@
_language_name: "Português (Portugal)"
_language_code: "P2"
_order: "27"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "A INICIAR"
STR_SLEEPING: "EM REPOUSO"
STR_ENTERING_SLEEP: "A entrar em repouso"
STR_BROWSE_FILES: "Explorar ficheiros"
STR_FILE_TRANSFER: "Transferência de ficheiros"
STR_SETTINGS_TITLE: "Definições"
STR_CONTINUE_READING: "Continuar a ler"
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
STR_START_READING: "Comece a ler abaixo"
STR_NO_FILES_FOUND: "Nenhum ficheiro encontrado"
STR_SELECT_CHAPTER: "Selecionar capítulo"
STR_NO_CHAPTERS: "Sem capítulos"
STR_END_OF_BOOK: "Fim do livro"
STR_EMPTY_CHAPTER: "Capítulo vazio"
STR_INDEXING: "A indexar"
STR_INDEX_FAILED: "Falha ao indexar - livro inválido"
STR_MEMORY_ERROR: "Erro de memória"
STR_PAGE_LOAD_ERROR: "Erro ao carregar a página"
STR_EMPTY_FILE: "Ficheiro vazio"
STR_OUT_OF_BOUNDS: "Fora dos limites"
STR_LOADING: "A carregar..."
STR_LOADING_POPUP: "A carregar"
STR_WIFI_NETWORKS: "Redes Wi-Fi"
STR_NO_NETWORKS: "Nenhuma rede encontrada"
STR_NETWORKS_FOUND: "%zu redes encontradas"
STR_SCANNING: "A procurar..."
STR_FINDING_SAVED_WIFI: "A procurar Wi-Fi guardado..."
STR_CONNECTING: "A ligar..."
STR_CONNECTING_SAVED_WIFI: "A ligar ao Wi-Fi guardado..."
STR_SHOW_NETWORKS: "Mostrar"
STR_CONNECTED: "Ligado!"
STR_CONNECTION_FAILED: "Falha na ligação"
STR_FORGET_NETWORK: "Esquecer rede?"
STR_SAVE_PASSWORD: "Guardar palavra-passe para a próxima vez?"
STR_PRESS_OK_SCAN: "Prima OK para procurar novamente"
STR_JOIN_NETWORK: "Aderir a uma rede"
STR_CREATE_HOTSPOT: "Criar ponto de acesso"
STR_JOIN_DESC: "Ligue-se a uma rede Wi-Fi existente"
STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas se ligarem"
STR_STARTING_HOTSPOT: "A iniciar ponto de acesso..."
STR_HOTSPOT_MODE: "Modo de ponto de acesso"
STR_CONNECT_WIFI_HINT: "Ligue o seu dispositivo a esta rede Wi-Fi"
STR_OPEN_URL_HINT: "Abra este URL no seu navegador"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou leia o código QR com o seu telemóvel:"
STR_CALIBRE_WIRELESS: "Calibre sem fios"
STR_NETWORK_LEGEND: "* = Encriptada | + = Guardada"
STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "A verificar o Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Introduza a palavra-passe do Wi-Fi"
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
STR_ENTER_WIFI_SSID: "Introduza o nome da rede (SSID)"
STR_TO_PREFIX: "para "
STR_CALIBRE_RECEIVING: "A receber: "
STR_CALIBRE_RECEIVED: "Recebido: "
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar para o dispositivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha este ecrã aberto durante o envio\""
STR_CAT_DISPLAY: "Ecrã"
STR_CAT_READER: "Leitor"
STR_CAT_CONTROLS: "Controlos"
STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Ecrã de repouso"
STR_QUICK_RESUME_TIMEOUT: "Retoma rápida após tempo limite"
STR_SLEEP_COVER_MODE: "Modo de capa no ecrã de repouso"
STR_HIDE_BATTERY: "Ocultar % da bateria"
STR_EXTRA_SPACING: "Espaçamento extra entre parágrafos"
STR_TEXT_AA: "Suavização do texto"
STR_IMAGES: "Imagens"
STR_IMAGES_DISPLAY: "Exibição"
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
STR_IMAGES_SUPPRESS: "Suprimir"
STR_EOB_HOME: "Início"
STR_EOB_CONTINUE_WITH: "Continuar com"
STR_SHORT_PWR_BTN: "Clique curto no botão de energia"
STR_ORIENTATION: "Orientação de leitura"
STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais (leitor)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
STR_LONG_PRESS_BEHAVIOR: "Comportamento de pressão longa"
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
STR_LONG_PRESS_MENU: "Pressão longa no Menu"
STR_FONT_PREVIEW_TEXT: "A rápida raposa castanha salta por cima do cão preguiçoso"
STR_FONT_FAMILY: "Tipo de letra do leitor"
STR_FONT_SIZE: "Tamanho do tipo de letra"
STR_LINE_SPACING: "Espaçamento entre linhas"
STR_SCREEN_MARGIN: "Margens do ecrã"
STR_PARA_ALIGNMENT: "Alinhamento dos parágrafos"
STR_HYPHENATION: "Hifenização"
STR_TIME_TO_SLEEP: "Tempo até repousar"
STR_SHOW_HIDDEN_FILES: "Mostrar ficheiros ocultos"
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
STR_MOVE_FINISHED_TO_READ: "Mover livros concluídos para a pasta Read"
STR_REFRESH_FREQ: "Frequência de atualização"
STR_KOREADER_SYNC: "Sincronização KOReader"
STR_CHECK_UPDATES: "Procurar atualizações"
STR_LANGUAGE: "Idioma"
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
STR_USERNAME: "Nome de utilizador"
STR_PASSWORD: "Palavra-passe"
STR_SYNC_SERVER_URL: "URL do servidor de sincronização"
STR_DOCUMENT_MATCHING: "Correspondência de documentos"
STR_SEND_METADATA: "Enviar metadados do documento"
STR_AUTHENTICATE: "Autenticar"
STR_KOREADER_USERNAME: "Utilizador do KOReader"
STR_KOREADER_PASSWORD: "Palavra-passe do KOReader"
STR_FILENAME: "Nome do ficheiro"
STR_BINARY: "Binário"
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
STR_WIFI_CONN_FAILED: "Falha na ligação Wi-Fi"
STR_AUTHENTICATING: "A autenticar..."
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
STR_KOREADER_AUTH: "Autenticação KOReader"
STR_SYNC_READY: "A sincronização KOReader está pronta a usar"
STR_AUTH_FAILED: "Falha na autenticação"
STR_DONE: "Concluído"
STR_CLEAR_CACHE_WARNING_1: "Isto irá limpar todos os dados de livros em cache."
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
STR_CLEAR_CACHE_WARNING_3: "Os livros terão de ser reindexados"
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
STR_CLEARING_CACHE: "A limpar a cache..."
STR_CACHE_CLEARED: "Cache limpa"
STR_ITEMS_REMOVED: "itens removidos"
STR_FAILED_LOWER: "falhou"
STR_CLEAR_CACHE_FAILED: "Falha ao limpar a cache"
STR_CHECK_SERIAL_OUTPUT: "Verifique a saída de série para obter detalhes"
STR_DARK: "Escuro"
STR_LIGHT: "Claro"
STR_CUSTOM: "Personalizado"
STR_COVER: "Capa"
STR_NONE_OPT: "Nenhum"
STR_FIT: "Ajustar"
STR_CROP: "Recortar"
STR_NEVER: "Nunca"
STR_IN_READER: "No leitor"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página"
STR_FORCE_REFRESH: "Atualizar ecrã"
STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem (Dir.)"
STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Retrato 180°"
STR_LANDSCAPE_CCW: "Paisagem (Esq.)"
STR_PREV_NEXT: "Ant/Seg"
STR_NEXT_PREV: "Seg/Ant"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Marcador"
STR_DISABLED: "Desativado"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Pequeno"
STR_MEDIUM: "Médio"
STR_LARGE: "Grande"
STR_X_LARGE: "Extra grande"
STR_TIGHT: "Apertado"
STR_NORMAL: "Normal"
STR_WIDE: "Largo"
STR_JUSTIFY: "Justificar"
STR_ALIGN_LEFT: "Esquerda"
STR_CENTER: "Centrar"
STR_ALIGN_RIGHT: "Direita"
STR_PAGES_1: "1 página"
STR_PAGES_5: "5 páginas"
STR_PAGES_10: "10 páginas"
STR_PAGES_15: "15 páginas"
STR_PAGES_30: "30 páginas"
STR_UPDATE: "Atualizar"
STR_CHECKING_UPDATE: "A procurar atualização..."
STR_NEW_UPDATE: "Nova atualização disponível!"
STR_CURRENT_VERSION: "Versão atual: "
STR_NEW_VERSION: "Nova versão: "
STR_UPDATING: "A atualizar..."
STR_NO_UPDATE: "Nenhuma atualização disponível"
STR_UPDATE_FAILED: "Falha na atualização"
STR_UPDATE_COMPLETE: "Atualização concluída"
STR_POWER_ON_HINT: "Prima sem soltar o botão de energia para voltar a ligar"
STR_RESTARTING_HINT: "A reiniciar... Se o dispositivo não reiniciar, mantenha premido o botão de energia durante alguns segundos."
STR_NO_ENTRIES: "Nenhum registo encontrado"
STR_DOWNLOADING: "A transferir..."
STR_DOWNLOAD_FAILED: "Falha na transferência"
STR_ERROR_MSG: "Erro:"
STR_UNNAMED: "Sem nome"
STR_HOLD_OPEN_TO_DELETE: "Mantenha premido Abrir para eliminar"
STR_NO_SERVER_URL: "Nenhum URL de servidor configurado"
STR_FETCH_FEED_FAILED: "Falha ao obter o feed"
STR_PARSE_FEED_FAILED: "Falha ao analisar o feed"
STR_NEXT_PAGE: "Página seguinte »"
STR_PREV_PAGE: "« Página anterior"
STR_NETWORK_PREFIX: "Rede: "
STR_IP_ADDRESS_PREFIX: "Endereço IP: "
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite de ligação esgotado"
STR_SD_CARD: "Cartão SD"
STR_BACK: "« Voltar"
STR_EXIT: "« Sair"
STR_HOME: "« Início"
STR_SELECT: "Selecionar"
STR_SELECTED: "Selecionado"
STR_TOGGLE: "Alternar"
STR_TOGGLE_BOOKMARK: "Alternar marcador"
STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar"
STR_CONNECT: "Ligar"
STR_OPEN: "Abrir"
STR_DOWNLOAD: "Transferir"
STR_RETRY: "Tentar novamente"
STR_YES: "Sim"
STR_NO: "Não"
STR_SHOW: "Mostrar"
STR_HIDE: "Ocultar"
STR_STATE_ON: "LIG."
STR_STATE_OFF: "DESL."
STR_NOT_SET: "Não definido"
STR_DIR_LEFT: "Esquerda"
STR_DIR_RIGHT: "Direita"
STR_DIR_UP: "Cima"
STR_DIR_DOWN: "Baixo"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtro da capa de ecrã de repouso"
STR_FILTER_CONTRAST: "Contraste"
STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado"
STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo"
STR_BOOK_PROGRESS_PERCENTAGE: "Percentagem de progresso do livro"
STR_PROGRESS_BAR: "Barra de progresso"
STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso"
STR_PROGRESS_BAR_THIN: "Fina"
STR_PROGRESS_BAR_MEDIUM: "Média"
STR_PROGRESS_BAR_THICK: "Grossa"
STR_BOOK: "Livro"
STR_CHAPTER: "Capítulo"
STR_EXAMPLE_CHAPTER: "Capítulo 21"
STR_EXAMPLE_BOOK: "Título do livro"
STR_PREVIEW: "Pré-visualização"
STR_TITLE: "Título"
STR_BATTERY: "Bateria"
STR_XTC_STATUS_BAR: "Barra de estado XTC"
STR_BOTTOM: "Inferior"
STR_TOP: "Superior"
STR_CLOCK: "Relógio"
STR_CLOCK_UTC_OFFSET: "Deslocamento UTC do relógio"
STR_CLOCK_FORMAT: "Formato do relógio"
STR_CLOCK_FORMAT_24H: "24 horas"
STR_CLOCK_FORMAT_12H: "12 horas"
STR_CURRENT_TIME: "Hora atual:"
STR_NEXT_FIELD: "Seguinte"
STR_CLOCK_SYNC: "Sincronizar relógio"
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
STR_CLOCK_SYNCING: "A sincronizar via NTP..."
STR_CLOCK_SYNC_OK: "Relógio sincronizado"
STR_CLOCK_SYNC_FAIL: "Falha na sincronização"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não ligado"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Ligue-se primeiro ao Wi-Fi e, em seguida, tente novamente."
STR_CLOCK_SYNCED: "Relógio sincronizado"
STR_UI_THEME: "Tema da interface"
STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Ajuste de desbotamento ao sol"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_BOOKMARKS: "Marcadores"
STR_BOOKMARK_ADDED: "Marcador adicionado."
STR_BOOKMARK_REMOVED: "Marcador removido."
STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Pesquisar"
STR_COVER_CUSTOM: "Capa + Personalizado"
STR_QUICK_RESUME: "Retoma rápida"
STR_MENU_RECENT_BOOKS: "Livros recentes"
STR_REMOVE_FROM_RECENTS: "Remover dos Livros recentes?"
STR_NO_RECENT_BOOKS: "Nenhum livro recente"
STR_CALIBRE_DESC: "Usar transferências sem fios de dispositivos do Calibre"
STR_FORGET_AND_REMOVE: "Esquecer rede e remover palavra-passe guardada?"
STR_FORGET_BUTTON: "Esquecer"
STR_CALIBRE_STARTING: "A iniciar o Calibre..."
STR_CALIBRE_SETUP: "Configuração"
STR_CALIBRE_STATUS: "Estado"
STR_CLEAR_BUTTON: "Limpar"
STR_DEFAULT_VALUE: "Predefinição"
STR_REMAP_PROMPT: "Prima um botão frontal para cada função"
STR_UNASSIGNED: "Não atribuído"
STR_ALREADY_ASSIGNED: "Já atribuído"
STR_REMAP_RESET_HINT: "Botão lateral Cima: Repor disposição predefinida"
STR_REMAP_CANCEL_HINT: "Botão lateral Baixo: Cancelar remapeamento"
STR_HW_BACK_LABEL: "Voltar (1º botão)"
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
STR_GO_TO_PERCENT: "Ir para %"
STR_GO_HOME_BUTTON: "Ir para o Início"
STR_SYNC_PROGRESS: "Sincronizar progresso"
STR_DELETE_CACHE: "Eliminar cache do livro"
STR_DELETE: "Eliminar"
STR_CONFIRM_DELETE_BOOKMARK: "Eliminar este marcador?"
STR_DISPLAY_QR: "Mostrar página como QR"
STR_CHAPTER_PREFIX: "Capítulo: "
STR_PAGES_SEPARATOR: " páginas | "
STR_BOOK_PREFIX: "Livro: "
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
STR_SYNCING_TIME: "A sincronizar a hora..."
STR_CALC_HASH: "A calcular o hash do documento..."
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
STR_FETCH_PROGRESS: "A procurar progresso remoto..."
STR_UPLOAD_PROGRESS: "A enviar progresso..."
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader nas Definições"
STR_PROGRESS_FOUND: "Progresso encontrado!"
STR_REMOTE_LABEL: "Remoto:"
STR_LOCAL_LABEL: "Local:"
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
STR_DEVICE_FROM_FORMAT: " De: %s"
STR_APPLY_REMOTE: "Aplicar progresso remoto"
STR_UPLOAD_LOCAL: "Enviar progresso local"
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
STR_UPLOAD_PROMPT: "Enviar posição atual?"
STR_UPLOAD_SUCCESS: "Progresso enviado!"
STR_SYNC_FAILED_MSG: "Falha na sincronização"
STR_SAVE_PROGRESS_FAILED: "Não foi possível guardar o progresso"
STR_SECTION_PREFIX: "Secção "
STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido"
STR_FOCUS_READING: "Leitura focada"
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido das notas de rodapé"
STR_SET_SLEEP_COVER: "Definir capa"
STR_FOOTNOTES: "Notas de rodapé"
STR_NO_FOOTNOTES: "Nenhuma nota de rodapé nesta página"
STR_LINK: "[ligação]"
STR_SCREENSHOT_BUTTON: "Captura de ecrã"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nunca"
STR_STEP_HINT_FRONT: "Botões frontais:"
STR_STEP_HINT_SIDE: "Botões laterais:"
STR_ADD_SERVER: "Adicionar servidor"
STR_SERVER_NAME: "Nome do servidor"
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
STR_DELETE_SERVER: "Eliminar servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_AUTO_TURN_ENABLED: "Virar página automático ativado: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virar página automático (Páginas por minuto)"
STR_MANAGE_FONTS: "Gerir tipos de letra"
STR_FONT_BROWSER: "Navegador de tipos de letra"
STR_LOADING_FONT_LIST: "A carregar a lista de tipos de letra..."
STR_NO_FONTS_AVAILABLE: "Nenhum tipo de letra disponível"
STR_FONT_INSTALLED: "Tipo de letra instalado!"
STR_FONT_INSTALL_FAILED: "Falha na instalação do tipo de letra"
STR_INSTALLED: "Instalado"
STR_DOWNLOAD_ALL: "Transferir tudo"
STR_UPDATE_ALL: "Atualizar tudo"
STR_UPDATE_AVAILABLE: "Atualizar"
STR_CRASH_TITLE: "Falha do sistema"
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi guardado em crash_report.txt. Por favor, inclua este ficheiro no seu relatório de erro."
STR_CRASH_REASON: "Motivo da falha:"
STR_CRASH_NO_REASON: "(Nenhum motivo foi registado)"
STR_TILT_PAGE_TURN: "Virar página por inclinação"
STR_KB_HINT_MOVE_CURSOR: "Prima ESQUERDA ou DIREITA para mover o cursor"
STR_KB_HINT_RETURN_CURSOR: "Prima ESQUERDA para regressar à posição do cursor"
STR_KB_HINT_HIDE_PASSWORD: "Mantenha premido DIREITA e depois prima [***] para ocultar a palavra-passe"
STR_KB_HINT_SHOW_PASSWORD: "Mantenha premido DIREITA e depois prima [abc] para mostrar a palavra-passe"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prima [***] para ocultar a palavra-passe"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prima [abc] para mostrar a palavra-passe"
STR_KB_HINT_EDIT_ENTRY: "Mantenha premido CIMA para editar a entrada"
STR_KB_TIPS: "Dicas:"
STR_KB_HINT_RETURN_KEYBOARD: "Prima BAIXO para regressar ao teclado"
STR_KB_HINT_EXIT_URL_MODE: "Prima ABC para sair do modo URL"
STR_KB_HINT_CLEAR_TEXT: "Mantenha premido DEL para limpar todo o texto"
STR_KB_HINT_SECONDARY_CHAR: "Mantenha premido SELECT para o carácter secundário"
STR_KB_HINT_UPPER_SECONDARY: "Mantenha premido SELECT para MAIÚSCULAS ou carácter secundário"
STR_KB_HINT_LOWER_SECONDARY: "Mantenha premido SELECT para minúsculas ou carácter secundário"
STR_KB_HINT_URL_SNIPPETS: "Prima URL para atalhos"
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
STR_SELECT_FIRMWARE_FILE: "Selecione o ficheiro de firmware (.bin)"
STR_NO_BIN_FILES: "Nenhum ficheiro .bin encontrado"
STR_VALIDATING_FIRMWARE: "A validar o firmware..."
STR_INVALID_FIRMWARE: "Ficheiro de firmware inválido"
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande para a partição"
STR_FIRMWARE_TOO_SMALL: "Ficheiro de firmware demasiado pequeno"
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o ficheiro"
STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
STR_RECOVERY_MODE: "Modo de Recuperação"
STR_RECOVERY_MODE_HINT: "Coloque o ficheiro firmware.bin na raiz do cartão SD e selecione-o"
+3
View File
@@ -97,6 +97,7 @@ STR_USERNAME: "Utilizator"
STR_PASSWORD: "Parolă" STR_PASSWORD: "Parolă"
STR_SYNC_SERVER_URL: "URL server sincronizare" STR_SYNC_SERVER_URL: "URL server sincronizare"
STR_DOCUMENT_MATCHING: "Corespondenţă document" STR_DOCUMENT_MATCHING: "Corespondenţă document"
STR_SEND_METADATA: "Trimite metadate document"
STR_AUTHENTICATE: "Autentificare" STR_AUTHENTICATE: "Autentificare"
STR_KOREADER_USERNAME: "Nume utilizator KOReader" STR_KOREADER_USERNAME: "Nume utilizator KOReader"
STR_KOREADER_PASSWORD: "Parolă KOReader" STR_KOREADER_PASSWORD: "Parolă KOReader"
@@ -303,3 +304,5 @@ STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: " STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut" STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare" STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare"
STR_ADD_HIDDEN_NETWORK: "Adaugă rețea ascunsă..."
STR_ENTER_WIFI_SSID: "Introduceți numele rețelei (SSID)"
+3
View File
@@ -99,6 +99,7 @@ STR_USERNAME: "Имя пользователя"
STR_PASSWORD: "Пароль" STR_PASSWORD: "Пароль"
STR_SYNC_SERVER_URL: "URL сервера синхронизации" STR_SYNC_SERVER_URL: "URL сервера синхронизации"
STR_DOCUMENT_MATCHING: "Сопоставление документов" STR_DOCUMENT_MATCHING: "Сопоставление документов"
STR_SEND_METADATA: "Отправлять метаданные"
STR_AUTHENTICATE: "Авторизация" STR_AUTHENTICATE: "Авторизация"
STR_KOREADER_USERNAME: "Имя пользователя KOReader" STR_KOREADER_USERNAME: "Имя пользователя KOReader"
STR_KOREADER_PASSWORD: "Пароль KOReader" STR_KOREADER_PASSWORD: "Пароль KOReader"
@@ -383,3 +384,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ошибка записи прошивки"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
STR_RECOVERY_MODE: "Режим восстановления" STR_RECOVERY_MODE: "Режим восстановления"
STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его" STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его"
STR_ADD_HIDDEN_NETWORK: "Добавить скрытую сеть..."
STR_ENTER_WIFI_SSID: "Введите имя сети (SSID)"
+6 -3
View File
@@ -97,7 +97,8 @@ STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
STR_USERNAME: "Používateľské meno" STR_USERNAME: "Používateľské meno"
STR_PASSWORD: "Heslo" STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synchronizačného servera" STR_SYNC_SERVER_URL: "URL synchronizačného servera"
STR_DOCUMENT_MATCHING: "Párovanie dokumentov" STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
STR_SEND_METADATA: "Odosielať metadáta dokumentu"
STR_AUTHENTICATE: "Overiť" STR_AUTHENTICATE: "Overiť"
STR_KOREADER_USERNAME: "Používateľské meno KOReader" STR_KOREADER_USERNAME: "Používateľské meno KOReader"
STR_KOREADER_PASSWORD: "Heslo KOReader" STR_KOREADER_PASSWORD: "Heslo KOReader"
@@ -327,8 +328,8 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky" STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy" STR_SLEEP_NEVER: "Nikdy"
STR_STEP_HINT_FRONT: "Predné tlačidlá:" STR_STEP_HINT_FRONT: "Predné tlačidlá:"
STR_STEP_HINT_SIDE: "Bočné tlačidlá:" STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
STR_ADD_SERVER: "Pridať server" STR_ADD_SERVER: "Pridať server"
STR_SERVER_NAME: "Názov servera" STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery" STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
@@ -379,3 +380,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
STR_RECOVERY_MODE: "Režim obnovenia" STR_RECOVERY_MODE: "Režim obnovenia"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho" STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
STR_ADD_HIDDEN_NETWORK: "Pridať skrytú sieť..."
STR_ENTER_WIFI_SSID: "Zadajte názov siete (SSID)"
+3
View File
@@ -94,6 +94,7 @@ STR_USERNAME: "Uporabniško ime"
STR_PASSWORD: "Geslo" STR_PASSWORD: "Geslo"
STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo" STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov" STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
STR_SEND_METADATA: "Pošlji metapodatke dokumenta"
STR_AUTHENTICATE: "Avtentikacija" STR_AUTHENTICATE: "Avtentikacija"
STR_KOREADER_USERNAME: "KOReader uporabnik" STR_KOREADER_USERNAME: "KOReader uporabnik"
STR_KOREADER_PASSWORD: "KOReader geslo" STR_KOREADER_PASSWORD: "KOReader geslo"
@@ -300,3 +301,5 @@ STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: " STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)" STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
STR_TILT_PAGE_TURN: "Obračanje s priklonom" STR_TILT_PAGE_TURN: "Obračanje s priklonom"
STR_ADD_HIDDEN_NETWORK: "Dodaj skrito omrežje..."
STR_ENTER_WIFI_SSID: "Vnesite ime omrežja (SSID)"
+9
View File
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sin capítulos"
STR_END_OF_BOOK: "Fin del libro" STR_END_OF_BOOK: "Fin del libro"
STR_EMPTY_CHAPTER: "Capítulo vacío" STR_EMPTY_CHAPTER: "Capítulo vacío"
STR_INDEXING: "Indexando" STR_INDEXING: "Indexando"
STR_INDEX_FAILED: "No se pudo indexar: libro no válido"
STR_MEMORY_ERROR: "Error de memoria" STR_MEMORY_ERROR: "Error de memoria"
STR_PAGE_LOAD_ERROR: "Error al cargar la página" STR_PAGE_LOAD_ERROR: "Error al cargar la página"
STR_EMPTY_FILE: "Archivo vacío" STR_EMPTY_FILE: "Archivo vacío"
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Redes Wi-Fi"
STR_NO_NETWORKS: "No hay redes disponibles" STR_NO_NETWORKS: "No hay redes disponibles"
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)" STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
STR_SCANNING: "Buscando..." STR_SCANNING: "Buscando..."
STR_FINDING_SAVED_WIFI: "Buscando redes Wi-Fi guardadas..."
STR_CONNECTING: "Conectando..." STR_CONNECTING: "Conectando..."
STR_CONNECTING_SAVED_WIFI: "Conectando a una red Wi-Fi guardada..."
STR_SHOW_NETWORKS: "Mostrar"
STR_CONNECTED: "¡Conectado!" STR_CONNECTED: "¡Conectado!"
STR_CONNECTION_FAILED: "Error de conexión" STR_CONNECTION_FAILED: "Error de conexión"
STR_FORGET_NETWORK: "¿Olvidar la red?" STR_FORGET_NETWORK: "¿Olvidar la red?"
@@ -69,6 +73,8 @@ STR_IMAGES: "Imágenes"
STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Reemplazar" STR_IMAGES_PLACEHOLDER: "Reemplazar"
STR_IMAGES_SUPPRESS: "Ocultar" STR_IMAGES_SUPPRESS: "Ocultar"
STR_EOB_HOME: "Inicio"
STR_EOB_CONTINUE_WITH: "Continuar con"
STR_SHORT_PWR_BTN: "Toque corto del encendido" STR_SHORT_PWR_BTN: "Toque corto del encendido"
STR_ORIENTATION: "Orientación" STR_ORIENTATION: "Orientación"
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)" STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
@@ -98,6 +104,7 @@ STR_USERNAME: "Usuario"
STR_PASSWORD: "Contraseña" STR_PASSWORD: "Contraseña"
STR_SYNC_SERVER_URL: "URL del servidor de sinc." STR_SYNC_SERVER_URL: "URL del servidor de sinc."
STR_DOCUMENT_MATCHING: "Coincidencia de doc." STR_DOCUMENT_MATCHING: "Coincidencia de doc."
STR_SEND_METADATA: "Enviar metadatos doc."
STR_AUTHENTICATE: "Autenticar" STR_AUTHENTICATE: "Autenticar"
STR_KOREADER_USERNAME: "Usuario de KOReader" STR_KOREADER_USERNAME: "Usuario de KOReader"
STR_KOREADER_PASSWORD: "Contraseña de KOReader" STR_KOREADER_PASSWORD: "Contraseña de KOReader"
@@ -383,3 +390,5 @@ STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!"
STR_RECOVERY_MODE: "Modo de recuperación" STR_RECOVERY_MODE: "Modo de recuperación"
STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo" STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo"
STR_ADD_HIDDEN_NETWORK: "Añadir red oculta..."
STR_ENTER_WIFI_SSID: "Introduce el nombre de la red (SSID)"
+12 -3
View File
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Inga kapitel"
STR_END_OF_BOOK: "Slutet på boken" STR_END_OF_BOOK: "Slutet på boken"
STR_EMPTY_CHAPTER: "Tomt kapitel" STR_EMPTY_CHAPTER: "Tomt kapitel"
STR_INDEXING: "Indexerar" STR_INDEXING: "Indexerar"
STR_INDEX_FAILED: "Misslyckades att indexera - ogiltig bok"
STR_MEMORY_ERROR: "Minnesfel" STR_MEMORY_ERROR: "Minnesfel"
STR_PAGE_LOAD_ERROR: "Sidladdningsfel" STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
STR_EMPTY_FILE: "Tom fil" STR_EMPTY_FILE: "Tom fil"
@@ -61,7 +62,7 @@ STR_CAT_READER: "Läsare"
STR_CAT_CONTROLS: "Kontroller" STR_CAT_CONTROLS: "Kontroller"
STR_CAT_SYSTEM: "System" STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Viloskärm" STR_SLEEP_SCREEN: "Viloskärm"
STR_SEAMLESS_SLEEP: "Sida som viloskärm" STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge" STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
STR_HIDE_BATTERY: "Dölj batteriprocent" STR_HIDE_BATTERY: "Dölj batteriprocent"
STR_EXTRA_SPACING: "Extra paragrafmellanrum" STR_EXTRA_SPACING: "Extra paragrafmellanrum"
@@ -70,6 +71,8 @@ STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Visa" STR_IMAGES_DISPLAY: "Visa"
STR_IMAGES_PLACEHOLDER: "Platshållare" STR_IMAGES_PLACEHOLDER: "Platshållare"
STR_IMAGES_SUPPRESS: "Dölj" STR_IMAGES_SUPPRESS: "Dölj"
STR_EOB_HOME: "Hem"
STR_EOB_CONTINUE_WITH: "Fortsätt med"
STR_SHORT_PWR_BTN: "Kort strömknappsklick" STR_SHORT_PWR_BTN: "Kort strömknappsklick"
STR_ORIENTATION: "Läsrikting" STR_ORIENTATION: "Läsrikting"
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)" STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
@@ -78,6 +81,7 @@ STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
STR_LONG_PRESS_BEHAVIOR_OFF: "AV" STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel" STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering"
STR_LONG_PRESS_MENU: "Långtrycksmeny"
STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor" STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor"
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj" STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek" STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
@@ -98,6 +102,7 @@ STR_USERNAME: "Användarnamn"
STR_PASSWORD: "Lösenord" STR_PASSWORD: "Lösenord"
STR_SYNC_SERVER_URL: "Synkronisera serveradress" STR_SYNC_SERVER_URL: "Synkronisera serveradress"
STR_DOCUMENT_MATCHING: "Dokumentmatchning" STR_DOCUMENT_MATCHING: "Dokumentmatchning"
STR_SEND_METADATA: "Skicka dokumentmetadata"
STR_AUTHENTICATE: "Autentisera " STR_AUTHENTICATE: "Autentisera "
STR_KOREADER_USERNAME: "KOReader användarnamn" STR_KOREADER_USERNAME: "KOReader användarnamn"
STR_KOREADER_PASSWORD: "KOReader lösenord" STR_KOREADER_PASSWORD: "KOReader lösenord"
@@ -142,6 +147,8 @@ STR_ORIENTATION_INVERTED: "Porträtt 180°"
STR_LANDSCAPE_CCW: "Landskap moturs" STR_LANDSCAPE_CCW: "Landskap moturs"
STR_PREV_NEXT: "Förra/Nästa" STR_PREV_NEXT: "Förra/Nästa"
STR_NEXT_PREV: "Nästa/Förra" STR_NEXT_PREV: "Nästa/Förra"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bokmärke"
STR_DISABLED: "Inaktiverad" STR_DISABLED: "Inaktiverad"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
@@ -255,7 +262,6 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra utökad" STR_THEME_LYRA_EXTENDED: "Lyra utökad"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning" STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_BOOKMARKS: "Bokmärken" STR_BOOKMARKS: "Bokmärken"
STR_BOOKMARK_ADDED: "Bokmärke tillagt." STR_BOOKMARK_ADDED: "Bokmärke tillagt."
@@ -321,15 +327,16 @@ STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil" STR_EMBEDDED_STYLE: "Inbäddad stil"
STR_FOCUS_READING: "Fokusläsning" STR_FOCUS_READING: "Fokusläsning"
STR_OPDS_SERVER_URL: "OPDS-serveradress" STR_OPDS_SERVER_URL: "OPDS-serveradress"
STR_PWR_BTN_FOOTNOTE_BACK: "Snabbåtergång från fotnoter"
STR_SET_SLEEP_COVER: "Ställ in omslag" STR_SET_SLEEP_COVER: "Ställ in omslag"
STR_FOOTNOTES: "Fotnoter" STR_FOOTNOTES: "Fotnoter"
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan" STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
STR_LINK: "[länk]" STR_LINK: "[länk]"
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig" STR_SLEEP_NEVER: "Aldrig"
STR_STEP_HINT_FRONT: "Framknappar:" STR_STEP_HINT_FRONT: "Framknappar:"
STR_STEP_HINT_SIDE: "Sidoknappar:" STR_STEP_HINT_SIDE: "Sidoknappar:"
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
STR_ADD_SERVER: "Lägg till server" STR_ADD_SERVER: "Lägg till server"
STR_SERVER_NAME: "Servernamn" STR_SERVER_NAME: "Servernamn"
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade" STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
@@ -380,3 +387,5 @@ STR_FIRMWARE_WRITE_FAILED: "Skrivning till firmware misslyckades"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Stäng inte av!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Stäng inte av!"
STR_RECOVERY_MODE: "Återställningsläge" STR_RECOVERY_MODE: "Återställningsläge"
STR_RECOVERY_MODE_HINT: "Placera firmware.bin i SD-kortroten och välj den" STR_RECOVERY_MODE_HINT: "Placera firmware.bin i SD-kortroten och välj den"
STR_ADD_HIDDEN_NETWORK: "Lägg till dolt nätverk..."
STR_ENTER_WIFI_SSID: "Ange nätverksnamn (SSID)"
+3
View File
@@ -92,6 +92,7 @@ STR_USERNAME: "Kullanıcı Adı"
STR_PASSWORD: "Şifre" STR_PASSWORD: "Şifre"
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi" STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
STR_DOCUMENT_MATCHING: "Belge Eşleştirme" STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
STR_SEND_METADATA: "Belge üst verisi gönder"
STR_AUTHENTICATE: "Kimlik Doğrula" STR_AUTHENTICATE: "Kimlik Doğrula"
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı" STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
STR_KOREADER_PASSWORD: "KOReader Şifresi" STR_KOREADER_PASSWORD: "KOReader Şifresi"
@@ -303,3 +304,5 @@ STR_SELECTED: "Seçili"
STR_SHOW: "Göster" STR_SHOW: "Göster"
STR_TITLE: "Başlık" STR_TITLE: "Başlık"
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme" STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
+3
View File
@@ -98,6 +98,7 @@ STR_USERNAME: "Ім'я користувача"
STR_PASSWORD: "Пароль" STR_PASSWORD: "Пароль"
STR_SYNC_SERVER_URL: "URL для синхронізації" STR_SYNC_SERVER_URL: "URL для синхронізації"
STR_DOCUMENT_MATCHING: "Порівняння документів" STR_DOCUMENT_MATCHING: "Порівняння документів"
STR_SEND_METADATA: "Надсилати метадані"
STR_AUTHENTICATE: "Автентифікувати" STR_AUTHENTICATE: "Автентифікувати"
STR_KOREADER_USERNAME: "Ім'я користувача KOReader" STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
STR_KOREADER_PASSWORD: "Пароль KOReader" STR_KOREADER_PASSWORD: "Пароль KOReader"
@@ -380,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
STR_RECOVERY_MODE: "Режим відновлення" STR_RECOVERY_MODE: "Режим відновлення"
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його" STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
STR_ADD_HIDDEN_NETWORK: "Додати приховану мережу..."
STR_ENTER_WIFI_SSID: "Введіть назву мережі (SSID)"
+65 -56
View File
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
STR_SLEEPING: "ENTRANT EN REPÒS" STR_SLEEPING: "ENTRANT EN REPÒS"
STR_ENTERING_SLEEP: "Entrant en repòs" STR_ENTERING_SLEEP: "Entrant en repòs"
STR_BROWSE_FILES: "Explora arxius" STR_BROWSE_FILES: "Explora arxius"
STR_FILE_TRANSFER: "Transferència" STR_FILE_TRANSFER: "Transferència d'arxius"
STR_SETTINGS_TITLE: "Configuració" STR_SETTINGS_TITLE: "Configuració"
STR_CONTINUE_READING: "Continua llegint" STR_CONTINUE_READING: "Continua llegint"
STR_NO_OPEN_BOOK: "Cap llibre obert" STR_NO_OPEN_BOOK: "Cap llibre obert"
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
STR_END_OF_BOOK: "Final del llibre" STR_END_OF_BOOK: "Final del llibre"
STR_EMPTY_CHAPTER: "Capítol buit" STR_EMPTY_CHAPTER: "Capítol buit"
STR_INDEXING: "S'està indexant" STR_INDEXING: "S'està indexant"
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
STR_MEMORY_ERROR: "Error de memòria" STR_MEMORY_ERROR: "Error de memòria"
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina" STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
STR_EMPTY_FILE: "Arxiu buit" STR_EMPTY_FILE: "Arxiu buit"
@@ -28,60 +29,65 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
STR_NO_NETWORKS: "No s'han trobat xarxes" STR_NO_NETWORKS: "No s'han trobat xarxes"
STR_NETWORKS_FOUND: "%zu xarxes trobades" STR_NETWORKS_FOUND: "%zu xarxes trobades"
STR_SCANNING: "S'està escanejant..." STR_SCANNING: "S'està escanejant..."
STR_FINDING_SAVED_WIFI: "S'estan buscant xarxes Wi-Fi guardades..."
STR_CONNECTING: "S'està connectant..." STR_CONNECTING: "S'està connectant..."
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi guardada..."
STR_SHOW_NETWORKS: "Mostra"
STR_CONNECTED: "S'ha connectat!" STR_CONNECTED: "S'ha connectat!"
STR_CONNECTION_FAILED: "Error de connexió" STR_CONNECTION_FAILED: "Error de connexió"
STR_FORGET_NETWORK: "Voleu oblidar esta xarxa?" STR_FORGET_NETWORK: "Vols oblidar esta xarxa?"
STR_SAVE_PASSWORD: "Voleu guardar la contrasenya per a la pròxima vegada?" STR_SAVE_PASSWORD: "Vols guardar la contrasenya per a la pròxima vegada?"
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar" STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
STR_JOIN_NETWORK: "Uneix-te a una xarxa" STR_JOIN_NETWORK: "Unix-te a una xarxa"
STR_CREATE_HOTSPOT: "Crea un punt d'accés" STR_CREATE_HOTSPOT: "Crea un punt d'accés"
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent" STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi" STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..." STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
STR_HOTSPOT_MODE: "Mode de punt d'accés" STR_HOTSPOT_MODE: "Mode de punt d'accés"
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a esta xarxa Wi-Fi" STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a esta xarxa Wi-Fi"
STR_OPEN_URL_HINT: "Obriu este URL al navegador" STR_OPEN_URL_HINT: "Obri este URL al navegador"
STR_OR_HTTP_PREFIX: "o http://" STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:" STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
STR_CALIBRE_WIRELESS: "Calibre sense fils" STR_CALIBRE_WIRELESS: "Calibre sense fils"
STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat" STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
STR_MAC_ADDRESS: "Adreça MAC:" STR_MAC_ADDRESS: "Adreça MAC:"
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..." STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi" STR_ENTER_WIFI_PASSWORD: "Introduïx la contrasenya Wi-Fi"
STR_TO_PREFIX: "a " STR_TO_PREFIX: "a "
STR_CALIBRE_RECEIVING: "S'està rebent: " STR_CALIBRE_RECEIVING: "S'està rebent: "
STR_CALIBRE_RECEIVED: "S'ha rebut: " STR_CALIBRE_RECEIVED: "S'ha rebut: "
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi" STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\"" STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu esta pantalla oberta mentre s'envia\"" STR_CALIBRE_INSTRUCTION_4: "\"Mantín esta pantalla oberta mentre s'envia\""
STR_CAT_DISPLAY: "Visualització" STR_CAT_DISPLAY: "Visualització"
STR_CAT_READER: "Lector" STR_CAT_READER: "Lector"
STR_CAT_CONTROLS: "Controls" STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "Sistema" STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Pantalla de repòs" STR_SLEEP_SCREEN: "Pantalla de repòs"
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps" STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs" STR_SLEEP_COVER_MODE: "Ajust de la portada en repòs"
STR_HIDE_BATTERY: "Oculta el % de bateria" STR_HIDE_BATTERY: "Oculta el % de bateria"
STR_EXTRA_SPACING: "Espaiat de paràgraf extra" STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
STR_TEXT_AA: "Antialiàsing del text" STR_TEXT_AA: "Antialiàsing del text"
STR_IMAGES: "Imatges" STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_PLACEHOLDER: "Text alternatiu"
STR_IMAGES_SUPPRESS: "Eliminar" STR_IMAGES_SUPPRESS: "Eliminar"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada" STR_EOB_HOME: "Inici"
STR_EOB_CONTINUE_WITH: "Continua amb"
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura" STR_ORIENTATION: "Orientació de lectura"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó" STR_LONG_PRESS_BEHAVIOR: "Acció en mantindre premut un botó"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
STR_LONG_PRESS_MENU: "Funció de pulsació llarga" STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!" STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
STR_FONT_FAMILY: "Família de fonts" STR_FONT_FAMILY: "Família de fonts"
STR_FONT_SIZE: "Grandària de la lletra (UI)" STR_FONT_SIZE: "Cos de lletra del lector"
STR_LINE_SPACING: "Interlineat del lector" STR_LINE_SPACING: "Interlineat del lector"
STR_SCREEN_MARGIN: "Marge de pantalla del lector" STR_SCREEN_MARGIN: "Marge de pantalla del lector"
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector" STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
@@ -94,21 +100,22 @@ STR_STEP_HINT_SIDE: "Botons laterals:"
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults" STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents" STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read" STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
STR_REFRESH_FREQ: "Freqüència de refresc" STR_REFRESH_FREQ: "Freqüència d'actualització"
STR_KOREADER_SYNC: "Sincronització del KOReader" STR_KOREADER_SYNC: "Sincronització del KOReader"
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions" STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
STR_LANGUAGE: "Idioma" STR_LANGUAGE: "Llengua"
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura" STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
STR_USERNAME: "Nom d'usuari" STR_USERNAME: "Nom d'usuari"
STR_PASSWORD: "Contrasenya" STR_PASSWORD: "Contrasenya"
STR_SYNC_SERVER_URL: "URL del servidor de sincronització" STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
STR_DOCUMENT_MATCHING: "Coincidència de documents" STR_DOCUMENT_MATCHING: "Coincidència de documents"
STR_SEND_METADATA: "Envia metadades del document"
STR_AUTHENTICATE: "Autentica" STR_AUTHENTICATE: "Autentica"
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader" STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
STR_KOREADER_PASSWORD: "Contrasenya del KOReader" STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
STR_FILENAME: "Nom d'arxiu" STR_FILENAME: "Nom d'arxiu"
STR_BINARY: "Binari" STR_BINARY: "Binari"
STR_SET_CREDENTIALS_FIRST: "Estableix les credencials primer" STR_SET_CREDENTIALS_FIRST: "Establix les credencials primer"
STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida" STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida"
STR_AUTHENTICATING: "S'està autenticant..." STR_AUTHENTICATING: "S'està autenticant..."
STR_AUTH_SUCCESS: "Autenticació correcta!" STR_AUTH_SUCCESS: "Autenticació correcta!"
@@ -148,7 +155,7 @@ STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
STR_KOSYNC: "KOSync" STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Punt de llibre" STR_BOOKMARK_OPTION: "Punt de llibre"
STR_DISABLED: "Desactivats" STR_DISABLED: "Sense funció"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Petita" STR_SMALL: "Petita"
@@ -176,16 +183,16 @@ STR_UPDATING: "S'està actualitzant..."
STR_NO_UPDATE: "No hi ha actualitzacions disponibles" STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
STR_UPDATE_FAILED: "Ha fallat l'actualització" STR_UPDATE_FAILED: "Ha fallat l'actualització"
STR_UPDATE_COMPLETE: "Actualització completada" STR_UPDATE_COMPLETE: "Actualització completada"
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar" STR_POWER_ON_HINT: "Prem i mantín premut el botó d'encesa per tornar a engegar"
STR_NO_ENTRIES: "No s'ha trobat cap entrada" STR_NO_ENTRIES: "No s'ha trobat cap entrada"
STR_DOWNLOADING: "S'està baixant..." STR_DOWNLOADING: "S'està baixant..."
STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Sense nom" STR_UNNAMED: "Sense nom"
STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar" STR_HOLD_OPEN_TO_DELETE: "Mantín premut Obri per esborrar"
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor" STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed" STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
STR_NETWORK_PREFIX: "Xarxa: " STR_NETWORK_PREFIX: "Xarxa: "
STR_IP_ADDRESS_PREFIX: "Adreça IP: " STR_IP_ADDRESS_PREFIX: "Adreça IP: "
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general" STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
@@ -198,11 +205,11 @@ STR_HOME: "« Inici"
STR_SELECT: "Selecciona" STR_SELECT: "Selecciona"
STR_SELECTED: "Seleccionat" STR_SELECTED: "Seleccionat"
STR_TOGGLE: "Canvia" STR_TOGGLE: "Canvia"
STR_TOGGLE_BOOKMARK: "Canvia punt de llibre" STR_TOGGLE_BOOKMARK: "Afig o elimina el punt de llibre"
STR_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
STR_CONNECT: "Connecta" STR_CONNECT: "Connecta"
STR_OPEN: "Obre" STR_OPEN: "Obri"
STR_DOWNLOAD: "Descarrega" STR_DOWNLOAD: "Descarrega"
STR_RETRY: "Reintenta" STR_RETRY: "Reintenta"
STR_YES: "Sí" STR_YES: "Sí"
@@ -217,7 +224,7 @@ STR_DIR_RIGHT: "Dreta"
STR_DIR_UP: "Amunt" STR_DIR_UP: "Amunt"
STR_DIR_DOWN: "Avall" STR_DIR_DOWN: "Avall"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs" STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
STR_FILTER_CONTRAST: "Contrast" STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat" STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol" STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
@@ -247,20 +254,20 @@ STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida" STR_QUICK_RESUME: "Represa ràpida"
STR_MENU_RECENT_BOOKS: "Llibres recents" STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Voleu eliminar-lo de Llibres recents?" STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents" STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre" STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre"
STR_FORGET_AND_REMOVE: "Voleu eliminar la contrasenya guardada?" STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i eliminar la contrasenya guardada?"
STR_FORGET_BUTTON: "Oblida" STR_FORGET_BUTTON: "Oblida"
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..." STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
STR_CALIBRE_SETUP: "Configura" STR_CALIBRE_SETUP: "Configura"
STR_CALIBRE_STATUS: "Estat" STR_CALIBRE_STATUS: "Estat"
STR_CLEAR_BUTTON: "Esborra" STR_CLEAR_BUTTON: "Esborra"
STR_DEFAULT_VALUE: "Per defecte" STR_DEFAULT_VALUE: "Per defecte"
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol" STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
STR_UNASSIGNED: "No assignat" STR_UNASSIGNED: "No assignat"
STR_ALREADY_ASSIGNED: "Ja assignat" STR_ALREADY_ASSIGNED: "Ja assignat"
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte" STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restablix la disposició per defecte"
STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació" STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació"
STR_HW_BACK_LABEL: "Arrere (1r botó)" STR_HW_BACK_LABEL: "Arrere (1r botó)"
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)" STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
@@ -271,19 +278,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
STR_SYNC_PROGRESS: "Sincronitza el progrés" STR_SYNC_PROGRESS: "Sincronitza el progrés"
STR_DELETE_CACHE: "Esborra la memòria cau del llibre" STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
STR_DELETE: "Esborra" STR_DELETE: "Esborra"
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar este punt de llibre?" STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar este punt de llibre?"
STR_DISPLAY_QR: "Mostra la pàgina com a QR" STR_DISPLAY_QR: "Mostra la pàgina com a QR"
STR_CHAPTER_PREFIX: "Capítol: " STR_CHAPTER_PREFIX: "Capítol: "
STR_PAGES_SEPARATOR: " pàgines | " STR_PAGES_SEPARATOR: " pàgines | "
STR_BOOK_PREFIX: "Llibre: " STR_BOOK_PREFIX: "Llibre: "
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL" STR_CALIBRE_URL_HINT: "Per al Calibre, afig /opds a la URL"
STR_SYNCING_TIME: "S'està sincronitzant el temps..." STR_SYNCING_TIME: "S'està sincronitzant el temps..."
STR_CALC_HASH: "S'està calculant el hash del document..." STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document" STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..." STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..." STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials" STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader en Configuració" STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader en Configuració"
STR_PROGRESS_FOUND: "S'ha trobat progrés!" STR_PROGRESS_FOUND: "S'ha trobat progrés!"
STR_REMOTE_LABEL: "Remot:" STR_REMOTE_LABEL: "Remot:"
STR_LOCAL_LABEL: "Local:" STR_LOCAL_LABEL: "Local:"
@@ -293,7 +300,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
STR_APPLY_REMOTE: "Aplica el progrés remot" STR_APPLY_REMOTE: "Aplica el progrés remot"
STR_UPLOAD_LOCAL: "Puja el progrés local" STR_UPLOAD_LOCAL: "Puja el progrés local"
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot" STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?" STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
STR_UPLOAD_SUCCESS: "Progrés pujat!" STR_UPLOAD_SUCCESS: "Progrés pujat!"
STR_SYNC_FAILED_MSG: "Sincronització fallida" STR_SYNC_FAILED_MSG: "Sincronització fallida"
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés" STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés"
@@ -308,11 +315,11 @@ STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina" STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
STR_LINK: "[enllaç]" STR_LINK: "[enllaç]"
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla" STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: " STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació" STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
STR_FORCE_REFRESH: "Refresca la pantalla" STR_FORCE_REFRESH: "Refresca la pantalla"
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons." STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantín premut el botó d'encesa durant uns segons."
STR_NEXT_PAGE: "Pàgina següent »" STR_NEXT_PAGE: "Pàgina següent »"
STR_PREV_PAGE: "« Pàgina anterior" STR_PREV_PAGE: "« Pàgina anterior"
STR_XTC_STATUS_BAR: "Barra d'estat XTC" STR_XTC_STATUS_BAR: "Barra d'estat XTC"
@@ -351,35 +358,37 @@ STR_INSTALLED: "Instal·lat"
STR_DOWNLOAD_ALL: "Descarrega-ho tot" STR_DOWNLOAD_ALL: "Descarrega-ho tot"
STR_UPDATE_ALL: "Actualitza-ho tot" STR_UPDATE_ALL: "Actualitza-ho tot"
STR_UPDATE_AVAILABLE: "Actualitza" STR_UPDATE_AVAILABLE: "Actualitza"
STR_CRASH_TITLE: "Bloqueig del sistema" STR_CRASH_TITLE: "Fallada del sistema"
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Incloeu este arxiu en l'informe d'errors." STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Inclou este arxiu en l'informe d'errors."
STR_CRASH_REASON: "Motiu del bloqueig:" STR_CRASH_REASON: "Motiu de la fallada"
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)" STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor" STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor" STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
STR_KB_HINT_HIDE_PASSWORD: "Mantén premut Dreta i prem [***] per ocultar la contrasenya" STR_KB_HINT_HIDE_PASSWORD: "Mantín premut Dreta i prem [***] per ocultar la contrasenya"
STR_KB_HINT_SHOW_PASSWORD: "Mantén premut Dreta i prem [abc] per mostrar la contrasenya" STR_KB_HINT_SHOW_PASSWORD: "Mantín premut Dreta i prem [abc] per mostrar la contrasenya"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya" STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya" STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya"
STR_KB_HINT_EDIT_ENTRY: "Mantén premut Amunt per editar l'entrada" STR_KB_HINT_EDIT_ENTRY: "Mantín premut Amunt per editar l'entrada"
STR_KB_TIPS: "Consells:" STR_KB_TIPS: "Consells:"
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat" STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL" STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL"
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text" STR_KB_HINT_CLEAR_TEXT: "Mantín premut DEL per esborrar tot el text"
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari" STR_KB_HINT_SECONDARY_CHAR: "Mantín premut SELECT: caràcter secundari"
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari" STR_KB_HINT_UPPER_SECONDARY: "Mantín premut SELECT: majúscules o caràcter secundari"
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari" STR_KB_HINT_LOWER_SECONDARY: "Mantín premut SELECT: minúscules o caràcter secundari"
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments" STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD" STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un arxiu de firmware (.bin)" STR_SELECT_FIRMWARE_FILE: "Selecciona un arxiu de firmware (.bin)"
STR_NO_BIN_FILES: "No s'han trobat arxius .bin" STR_NO_BIN_FILES: "No s'han trobat arxius .bin"
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..." STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid" STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició" STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut" STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?" STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu" STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware" STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagues el dispositiu!"
STR_RECOVERY_MODE: "Mode de recuperació" STR_RECOVERY_MODE: "Mode de recuperació"
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo" STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
STR_ADD_HIDDEN_NETWORK: "Afig una xarxa oculta..."
STR_ENTER_WIFI_SSID: "Introduïx el nom de la xarxa (SSID)"
+3
View File
@@ -98,6 +98,7 @@ STR_USERNAME: "Tên đăng nhập"
STR_PASSWORD: "Mật khẩu" STR_PASSWORD: "Mật khẩu"
STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ" STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
STR_DOCUMENT_MATCHING: "Khớp tài liệu" STR_DOCUMENT_MATCHING: "Khớp tài liệu"
STR_SEND_METADATA: "Gửi siêu dữ liệu tài liệu"
STR_AUTHENTICATE: "Xác thực" STR_AUTHENTICATE: "Xác thực"
STR_KOREADER_USERNAME: "Tên đăng nhập KOReader" STR_KOREADER_USERNAME: "Tên đăng nhập KOReader"
STR_KOREADER_PASSWORD: "Mật khẩu KOReader" STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
@@ -379,3 +380,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ghi firmware thất bại"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!"
STR_RECOVERY_MODE: "Chế độ phục hồi" STR_RECOVERY_MODE: "Chế độ phục hồi"
STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn" STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn"
STR_ADD_HIDDEN_NETWORK: "Thêm mạng ẩn..."
STR_ENTER_WIFI_SSID: "Nhập tên mạng (SSID)"
+5
View File
@@ -13,6 +13,11 @@ enum class InflateStatus {
// Streaming deflate decompressor wrapping uzlib. // Streaming deflate decompressor wrapping uzlib.
// //
// NOTE: retained ONLY for FontDecompressor's tiny one-shot flash-resident group
// decompressions, where uzlib's ~1KB state beats tinfl's ~11KB on the
// OOM-sensitive render path. All throughput paths (zip entries, PNG IDAT) use
// InflateStream (lib/miniz), which decodes several times faster.
//
// Two modes: // Two modes:
// init(false) — one-shot: input is a contiguous buffer, call read() once. // init(false) — one-shot: input is a contiguous buffer, call read() once.
// init(true) — streaming: allocates a 32KB ring buffer for back-references // init(true) — streaming: allocates a 32KB ring buffer for back-references
+28 -96
View File
@@ -1,118 +1,45 @@
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
#include <HalStorage.h>
#include <Logging.h> #include <Logging.h>
#include <MD5Builder.h> #include <MD5Builder.h>
#include <ObfuscationUtils.h> #include <ObfuscationUtils.h>
#include <Serialization.h>
#include "KOReaderJsonIO.h"
// Initialize the static instance
KOReaderCredentialStore KOReaderCredentialStore::instance;
namespace { namespace {
// File format version (for binary migration)
constexpr uint8_t KOREADER_FILE_VERSION = 1;
// File paths
constexpr char KOREADER_FILE_BIN[] = "/.crosspoint/koreader.bin";
constexpr char KOREADER_FILE_JSON[] = "/.crosspoint/koreader.json";
constexpr char KOREADER_FILE_BAK[] = "/.crosspoint/koreader.bin.bak";
// Default sync server URL // Default sync server URL
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443"; constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
// Legacy obfuscation key - "KOReader" in ASCII (only used for binary migration)
constexpr uint8_t LEGACY_OBFUSCATION_KEY[] = {0x4B, 0x4F, 0x52, 0x65, 0x61, 0x64, 0x65, 0x72};
constexpr size_t LEGACY_KEY_LENGTH = sizeof(LEGACY_OBFUSCATION_KEY);
void legacyDeobfuscate(std::string& data) {
for (size_t i = 0; i < data.size(); i++) {
data[i] ^= LEGACY_OBFUSCATION_KEY[i % LEGACY_KEY_LENGTH];
}
}
} // namespace } // namespace
bool KOReaderCredentialStore::saveToFile() const { void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
Storage.mkdir("/.crosspoint"); doc["username"] = getUsername();
return KOReaderJsonIO::save(*this, KOREADER_FILE_JSON); doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
doc["serverUrl"] = getServerUrl();
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
doc["sendMetadata"] = getSendMetadata();
} }
bool KOReaderCredentialStore::loadFromFile() { bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
// Try JSON first std::string user = doc["username"] | "";
if (Storage.exists(KOREADER_FILE_JSON)) {
String json = Storage.readFile(KOREADER_FILE_JSON);
if (!json.isEmpty()) {
bool resave = false;
bool result = KOReaderJsonIO::load(*this, json.c_str(), &resave);
if (result && resave) {
saveToFile();
LOG_DBG("KRS", "Resaved KOReader credentials to update format");
}
return result;
}
}
// Fall back to binary migration bool needsResave = false;
if (Storage.exists(KOREADER_FILE_BIN)) { std::string pass = extractPassword(doc, needsResave);
if (loadFromBinaryFile()) {
if (saveToFile()) {
Storage.rename(KOREADER_FILE_BIN, KOREADER_FILE_BAK);
LOG_DBG("KRS", "Migrated koreader.bin to koreader.json");
return true;
} else {
LOG_ERR("KRS", "Failed to save KOReader credentials during migration");
return false;
}
}
}
LOG_DBG("KRS", "No credentials file found"); setCredentials(user, pass);
return false; setServerUrl(doc["serverUrl"] | "");
}
bool KOReaderCredentialStore::loadFromBinaryFile() { uint8_t method = doc["matchMethod"] | (uint8_t)0;
HalFile file; if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) {
if (!Storage.openFileForRead("KRS", KOREADER_FILE_BIN, file)) { setMatchMethod(static_cast<DocumentMatchMethod>(method));
return false;
}
uint8_t version;
serialization::readPod(file, version);
if (version != KOREADER_FILE_VERSION) {
LOG_DBG("KRS", "Unknown file version: %u", version);
return false;
}
if (file.available()) {
serialization::readString(file, username);
} else { } else {
username.clear(); LOG_DBG("KRS", "Invalid matchMethod %u in JSON, resetting to FILENAME", method);
setMatchMethod(DocumentMatchMethod::FILENAME);
}
setSendMetadata(doc["sendMetadata"] | false);
if (needsResave) {
LOG_DBG("KRS", "Resaved KOReader credentials to update format");
saveToFile();
} }
if (file.available()) {
serialization::readString(file, password);
legacyDeobfuscate(password);
} else {
password.clear();
}
if (file.available()) {
serialization::readString(file, serverUrl);
} else {
serverUrl.clear();
}
if (file.available()) {
uint8_t method;
serialization::readPod(file, method);
matchMethod = static_cast<DocumentMatchMethod>(method);
} else {
matchMethod = DocumentMatchMethod::FILENAME;
}
LOG_DBG("KRS", "Loaded KOReader credentials from binary for user: %s", username.c_str());
return true; return true;
} }
@@ -173,3 +100,8 @@ void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
matchMethod = method; matchMethod = method;
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary"); LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
} }
void KOReaderCredentialStore::setSendMetadata(bool enabled) {
sendMetadata = enabled;
LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false");
}
+15 -13
View File
@@ -1,4 +1,7 @@
#pragma once #pragma once
#include <ArduinoJson.h>
#include <PersistableStore.h>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
@@ -14,30 +17,25 @@ enum class DocumentMatchMethod : uint8_t {
* and base64-encoded before writing to JSON (not cryptographically secure, * and base64-encoded before writing to JSON (not cryptographically secure,
* but prevents casual reading and ties credentials to the specific device). * but prevents casual reading and ties credentials to the specific device).
*/ */
class KOReaderCredentialStore {
class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore> {
private: private:
static KOReaderCredentialStore instance;
std::string username; std::string username;
std::string password; std::string password;
std::string serverUrl; // Custom sync server URL (empty = default) std::string serverUrl; // Custom sync server URL (empty = default)
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
bool sendMetadata = false; // Send document metadata with progress sync
// Private constructor for singleton // Private constructor for singleton
KOReaderCredentialStore() = default; KOReaderCredentialStore() = default;
~KOReaderCredentialStore() = default;
bool loadFromBinaryFile(); friend class PersistableStore<KOReaderCredentialStore>;
public: public:
// Delete copy constructor and assignment static const char* getFilePath() { return "/.crosspoint/koreader.json"; }
KOReaderCredentialStore(const KOReaderCredentialStore&) = delete; void toJson(JsonDocument& doc) const;
KOReaderCredentialStore& operator=(const KOReaderCredentialStore&) = delete; bool fromJson(JsonVariantConst doc);
// Get singleton instance
static KOReaderCredentialStore& getInstance() { return instance; }
// Save/load from SD card
bool saveToFile() const;
bool loadFromFile();
// Credential management // Credential management
void setCredentials(const std::string& user, const std::string& pass); void setCredentials(const std::string& user, const std::string& pass);
@@ -63,6 +61,10 @@ class KOReaderCredentialStore {
// Document matching method // Document matching method
void setMatchMethod(DocumentMatchMethod method); void setMatchMethod(DocumentMatchMethod method);
DocumentMatchMethod getMatchMethod() const { return matchMethod; } DocumentMatchMethod getMatchMethod() const { return matchMethod; }
// Send metadata setting
void setSendMetadata(bool enabled);
bool getSendMetadata() const { return sendMetadata; }
}; };
// Helper macro to access credential store // Helper macro to access credential store
-51
View File
@@ -1,51 +0,0 @@
#include "KOReaderJsonIO.h"
#include <ArduinoJson.h>
#include <HalStorage.h>
#include <Logging.h>
#include <ObfuscationUtils.h>
#include "KOReaderCredentialStore.h"
namespace KOReaderJsonIO {
bool save(const KOReaderCredentialStore& store, const char* path) {
JsonDocument doc;
doc["username"] = store.getUsername();
doc["password_obf"] = obfuscation::obfuscateToBase64(store.getPassword());
doc["serverUrl"] = store.getServerUrl();
doc["matchMethod"] = static_cast<uint8_t>(store.getMatchMethod());
String json;
serializeJson(doc, json);
return Storage.writeFile(path, json);
}
bool load(KOReaderCredentialStore& store, const char* json, bool* needsResave) {
if (needsResave) *needsResave = false;
JsonDocument doc;
auto error = deserializeJson(doc, json);
if (error) {
LOG_ERR("KRS", "JSON parse error: %s", error.c_str());
return false;
}
std::string user = doc["username"] | std::string("");
bool ok = false;
std::string pass = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
if (!ok || pass.empty()) {
pass = doc["password"] | std::string("");
if (!pass.empty() && needsResave) *needsResave = true;
}
store.setCredentials(user, pass);
store.setServerUrl(doc["serverUrl"] | std::string(""));
uint8_t method = doc["matchMethod"] | (uint8_t)0;
store.setMatchMethod(static_cast<DocumentMatchMethod>(method));
return true;
}
} // namespace KOReaderJsonIO
-8
View File
@@ -1,8 +0,0 @@
#pragma once
class KOReaderCredentialStore;
namespace KOReaderJsonIO {
bool save(const KOReaderCredentialStore& store, const char* path);
bool load(KOReaderCredentialStore& store, const char* json, bool* needsResave);
} // namespace KOReaderJsonIO
+98 -133
View File
@@ -2,10 +2,10 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <Logging.h> #include <Logging.h>
#include <esp_crt_bundle.h> #include <SecureHttpClient.h>
#include <esp_http_client.h> #include <base64.h>
#include <ctime> #include <string>
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
@@ -16,82 +16,49 @@ namespace {
constexpr char DEVICE_NAME[] = "CrossPoint"; constexpr char DEVICE_NAME[] = "CrossPoint";
constexpr char DEVICE_ID[] = "crosspoint-reader"; constexpr char DEVICE_ID[] = "crosspoint-reader";
// Small TLS buffers to fit in ESP32-C3's limited heap (~46KB free after WiFi). // KOSync's TLS-1.3 servers can't be reached through the precompiled system
// KOSync payloads are tiny JSON (<1KB), so 2KB buffers are sufficient. // mbedTLS (TLS 1.3 is stubbed out), so requests run over wolfSSL via
// Default 16KB buffers cause OOM during TLS handshake. // SecureHttpClient. The handshake still needs working heap; gate on it. wolfSSL's
constexpr int HTTP_BUF_SIZE = 2048; // footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
// floor. Check both total free heap and largest contiguous block so fragmented
// heap does not fall through into a failed TLS allocation path.
// MEMFIX-PORT: TLS heap gate; portable
// Field data (July 2026): launching sync from a reader session lands at
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
// so an optimistic attempt degrades to the same clean "sync failed" as the
// gate — the gate only needs to keep out states where a doomed handshake
// would waste tens of seconds, not guarantee success.
//
// Free and largest-block have separate requirements: with SP ECC
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
// a run of fast-math bignums. A handshake was measured succeeding inside a
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
// Cloudflare tunnels send a 3-cert Google Trust Services chain. During the TLS handshake // Apply the shared KOSync auth headers after begin(). x-auth-* is the native
// mbedTLS makes many small allocations that collectively consume ~48KB of heap. With only // KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
// ~50KB free after WiFi connects, the session drove min-free-ever down to 2600 bytes before void applyAuthHeaders(freeink::SecureHttpClient& http) {
// failing with MBEDTLS_ERR_X509_ALLOC_FAILED (-0x2880). Check total free heap (not max http.addHeader("Accept", "application/vnd.koreader.v1+json");
// contiguous block) because the failure mode is aggregate exhaustion, not one large alloc. http.addHeader("x-auth-user", KOREADER_STORE.getUsername());
constexpr uint32_t MIN_HEAP_FOR_TLS = 55000; http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password());
const std::string credentials = KOREADER_STORE.getUsername() + ":" + KOREADER_STORE.getPassword();
// Response buffer for reading HTTP body const String encoded = base64::encode(credentials.c_str());
struct ResponseBuffer { http.addHeader("Authorization", std::string("Basic ") + encoded.c_str());
char* data = nullptr;
int len = 0;
int capacity = 0;
~ResponseBuffer() { free(data); }
bool ensure(int size) {
if (size <= capacity) return true;
char* newData = (char*)realloc(data, size);
if (!newData) return false;
data = newData;
capacity = size;
return true;
}
};
// HTTP event handler to collect response body
esp_err_t httpEventHandler(esp_http_client_event_t* evt) {
auto* buf = static_cast<ResponseBuffer*>(evt->user_data);
if (evt->event_id == HTTP_EVENT_ON_DATA && buf) {
if (buf->ensure(buf->len + evt->data_len + 1)) {
memcpy(buf->data + buf->len, evt->data, evt->data_len);
buf->len += evt->data_len;
buf->data[buf->len] = '\0';
} else {
LOG_ERR("KOSync", "Response buffer allocation failed (%d bytes)", evt->data_len);
}
}
return ESP_OK;
} }
// Create configured esp_http_client with small TLS buffers // True when free heap is too low to risk a TLS handshake.
esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf, bool insufficientHeap() {
esp_http_client_method_t method = HTTP_METHOD_GET) { const uint32_t freeHeap = ESP.getFreeHeap();
esp_http_client_config_t config = {}; const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
config.url = url; if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) {
config.event_handler = httpEventHandler; LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap,
config.user_data = buf; MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS);
config.method = method; return true;
config.timeout_ms = 15000;
config.buffer_size = HTTP_BUF_SIZE;
config.buffer_size_tx = HTTP_BUF_SIZE;
config.crt_bundle_attach = esp_crt_bundle_attach;
// HTTP Basic Auth for Calibre-Web-Automated compatibility
config.username = KOREADER_STORE.getUsername().c_str();
config.password = KOREADER_STORE.getPassword().c_str();
config.auth_type = HTTP_AUTH_TYPE_BASIC;
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) return nullptr;
// KOSync auth headers
if (esp_http_client_set_header(client, "Accept", "application/vnd.koreader.v1+json") != ESP_OK ||
esp_http_client_set_header(client, "x-auth-user", KOREADER_STORE.getUsername().c_str()) != ESP_OK ||
esp_http_client_set_header(client, "x-auth-key", KOREADER_STORE.getMd5Password().c_str()) != ESP_OK) {
LOG_ERR("KOSync", "Failed to set auth headers");
esp_http_client_cleanup(client);
return nullptr;
} }
return false;
return client;
} }
} // namespace } // namespace
@@ -102,26 +69,24 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
return NO_CREDENTIALS; return NO_CREDENTIALS;
} }
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; const std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
const uint32_t freeHeap = ESP.getFreeHeap(); LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); if (insufficientHeap()) return LOW_MEMORY;
if (freeHeap < MIN_HEAP_FOR_TLS) {
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); freeink::SecureHttpClient http;
return LOW_MEMORY; http.setInsecure();
if (!http.begin(url)) {
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
return NETWORK_ERROR;
} }
applyAuthHeaders(http);
ResponseBuffer buf; const int httpCode = http.GET();
esp_http_client_handle_t client = createClient(url.c_str(), &buf); http.end();
if (!client) return NETWORK_ERROR;
esp_err_t err = esp_http_client_perform(client);
const int httpCode = esp_http_client_get_status_code(client);
lastHttpCode = httpCode; lastHttpCode = httpCode;
esp_http_client_cleanup(client);
LOG_DBG("KOSync", "Auth response: %d (err: %d)", httpCode, err); LOG_DBG("KOSync", "Auth response: %d", httpCode);
if (err != ESP_OK) return NETWORK_ERROR; if (httpCode <= 0) return NETWORK_ERROR;
if (httpCode == 200) return OK; if (httpCode == 200) return OK;
if (httpCode == 401) return AUTH_FAILED; if (httpCode == 401) return AUTH_FAILED;
return SERVER_ERROR; return SERVER_ERROR;
@@ -135,30 +100,31 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
return NO_CREDENTIALS; return NO_CREDENTIALS;
} }
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
const uint32_t freeHeap = ESP.getFreeHeap(); LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); if (insufficientHeap()) return LOW_MEMORY;
if (freeHeap < MIN_HEAP_FOR_TLS) {
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); freeink::SecureHttpClient http;
return LOW_MEMORY; http.setInsecure();
if (!http.begin(url)) {
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
return NETWORK_ERROR;
}
applyAuthHeaders(http);
const int httpCode = http.GET();
lastHttpCode = httpCode;
LOG_DBG("KOSync", "Get progress response: %d", httpCode);
if (httpCode <= 0) {
http.end();
return NETWORK_ERROR;
} }
ResponseBuffer buf; if (httpCode == 200) {
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
if (!client) return NETWORK_ERROR;
esp_err_t err = esp_http_client_perform(client);
const int httpCode = esp_http_client_get_status_code(client);
lastHttpCode = httpCode;
esp_http_client_cleanup(client);
LOG_DBG("KOSync", "Get progress response: %d (err: %d)", httpCode, err);
if (err != ESP_OK) return NETWORK_ERROR;
if (httpCode == 200 && buf.data) {
JsonDocument doc; JsonDocument doc;
const DeserializationError error = deserializeJson(doc, buf.data); const DeserializationError error = deserializeJson(doc, http.getString().c_str());
http.end();
if (error) { if (error) {
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str()); LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
@@ -176,6 +142,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
return OK; return OK;
} }
http.end();
if (httpCode == 401) return AUTH_FAILED; if (httpCode == 401) return AUTH_FAILED;
if (httpCode == 404) return NOT_FOUND; if (httpCode == 404) return NOT_FOUND;
return SERVER_ERROR; return SERVER_ERROR;
@@ -188,17 +155,19 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
return NO_CREDENTIALS; return NO_CREDENTIALS;
} }
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
const uint32_t freeHeap = ESP.getFreeHeap(); LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); if (insufficientHeap()) return LOW_MEMORY;
if (freeHeap < MIN_HEAP_FOR_TLS) {
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS);
return LOW_MEMORY;
}
// Build JSON body // Build JSON body
JsonDocument doc; JsonDocument doc;
doc["document"] = progress.document; doc["document"] = progress.document;
if (progress.metadata.has_value()) {
auto meta = doc["metadata"].to<JsonObject>();
meta["filename"] = progress.metadata->filename;
meta["title"] = progress.metadata->title;
meta["authors"] = progress.metadata->authors;
}
doc["progress"] = progress.progress; doc["progress"] = progress.progress;
doc["percentage"] = progress.percentage; doc["percentage"] = progress.percentage;
doc["device"] = DEVICE_NAME; doc["device"] = DEVICE_NAME;
@@ -209,25 +178,21 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
LOG_DBG("KOSync", "Request body: %s", body.c_str()); LOG_DBG("KOSync", "Request body: %s", body.c_str());
ResponseBuffer buf; freeink::SecureHttpClient http;
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT); http.setInsecure();
if (!client) return NETWORK_ERROR; if (!http.begin(url)) {
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
if (esp_http_client_set_header(client, "Content-Type", "application/json") != ESP_OK ||
esp_http_client_set_post_field(client, body.c_str(), body.length()) != ESP_OK) {
LOG_ERR("KOSync", "Failed to set request body");
esp_http_client_cleanup(client);
return NETWORK_ERROR; return NETWORK_ERROR;
} }
applyAuthHeaders(http);
esp_err_t err = esp_http_client_perform(client); http.addHeader("Content-Type", "application/json");
const int httpCode = esp_http_client_get_status_code(client); const int httpCode = http.sendRequest("PUT", body);
http.end();
lastHttpCode = httpCode; lastHttpCode = httpCode;
esp_http_client_cleanup(client);
LOG_DBG("KOSync", "Update progress response: %d (err: %d)", httpCode, err); LOG_DBG("KOSync", "Update progress response: %d", httpCode);
if (err != ESP_OK) return NETWORK_ERROR; if (httpCode <= 0) return NETWORK_ERROR;
if (httpCode == 200 || httpCode == 202) return OK; if (httpCode == 200 || httpCode == 202) return OK;
if (httpCode == 401) return AUTH_FAILED; if (httpCode == 401) return AUTH_FAILED;
return SERVER_ERROR; return SERVER_ERROR;
+20 -6
View File
@@ -1,16 +1,30 @@
#pragma once #pragma once
#include <cstdint>
#include <optional>
#include <string> #include <string>
/**
* Optional document metadata sent alongside progress sync requests.
* Mirrors the metadata object added in KOReader PR #15306.
* The official sync server ignores this field; custom servers may use it.
*/
struct KOReaderMetadata {
std::string filename; // e.g. "my_book.epub"
std::string title; // Document title from EPUB metadata
std::string authors; // Author(s) from EPUB metadata
};
/** /**
* Progress data from KOReader sync server. * Progress data from KOReader sync server.
*/ */
struct KOReaderProgress { struct KOReaderProgress {
std::string document; // Document hash std::string document; // Document hash
std::string progress; // XPath-like progress string std::string progress; // XPath-like progress string
float percentage; // Progress percentage (0.0 to 1.0) float percentage; // Progress percentage (0.0 to 1.0)
std::string device; // Device name std::string device; // Device name
std::string deviceId; // Device ID std::string deviceId; // Device ID
int64_t timestamp; // Unix timestamp of last update int64_t timestamp; // Unix timestamp of last update
std::optional<KOReaderMetadata> metadata; // Optional document metadata
}; };
/** /**
+51
View File
@@ -0,0 +1,51 @@
#include "BuildScratch.h"
#include <Logging.h>
#include <atomic>
namespace buildscratch {
namespace {
uint8_t* block = nullptr;
size_t blockLen = 0;
// atomic exchange so an opportunistic claim from another task can never
// double-hand-out the block (single core, but FreeRTOS preempts).
std::atomic<bool> claimed{false};
} // namespace
void lend(uint8_t* buf, const size_t len) {
if (block) {
LOG_ERR("SCR", "Build scratch lent twice; ignoring second lend");
return;
}
block = buf;
blockLen = len;
claimed.store(false);
}
void reclaim() {
if (claimed.load()) {
// A consumer still holds the block. The storage stays valid (it is the
// framebuffer allocation, never freed) but its contents are about to be
// clobbered; the consumer's output will be garbage. Loud log so a
// lifetime bug is visible instead of a silent corrupt decode.
LOG_ERR("SCR", "Build scratch reclaimed while still claimed");
}
block = nullptr;
blockLen = 0;
claimed.store(false);
}
uint8_t* claim(const size_t minLen, size_t* lenOut) {
if (!block || blockLen < minLen) return nullptr;
bool expected = false;
if (!claimed.compare_exchange_strong(expected, true)) return nullptr;
if (lenOut) *lenOut = blockLen;
return block;
}
void release(const uint8_t* p) {
if (p && p == block) claimed.store(false);
}
} // namespace buildscratch
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstddef>
#include <cstdint>
// Registry for the framebuffer bytes lent out during a build phase
// (GfxRenderer::FrameBufferLoan). The lender (GfxRenderer) deposits the block
// with lend()/reclaim(); a memory-hungry consumer (e.g. InflateStream's ~43KB
// tinfl state + window) may claim() it instead of allocating from the heap.
//
// Exactly one claimant at a time; claim() returns nullptr when the block is
// absent or already claimed, and consumers must fall back to the heap. The
// underlying storage is the framebuffer allocation itself, which is never
// freed -- so even the pathological case (reclaim() while still claimed, which
// logs an error) reads garbage, never freed memory.
namespace buildscratch {
// Lender side (GfxRenderer only).
void lend(uint8_t* buf, size_t len);
void reclaim();
// Consumer side: exclusive claim of the whole block if it is at least minLen
// bytes; nullptr means "use the heap". Release with the same pointer.
uint8_t* claim(size_t minLen, size_t* lenOut = nullptr);
void release(const uint8_t* p);
} // namespace buildscratch
+73 -1
View File
@@ -11,6 +11,13 @@ extern "C" {
#include <Utf8.h> #include <Utf8.h>
#include <cstring> #include <cstring>
#include <mutex>
// Guards the static bidi_char buffers in applyBidiVisual() and
// computeVisualWordOrder(). The bidi+shaping pipeline is not reentrant;
// this mutex serialises access so multi-core callers don't corrupt each
// other's intermediate state.
static std::mutex bidiMutex;
namespace { namespace {
@@ -68,11 +75,25 @@ int detectParagraphLevel(const char* utf8, const int fallbackLevel, const int ma
return fallbackLevel & 1; return fallbackLevel & 1;
} }
bool isTransparentMark(const uint32_t cp) {
// RTL-script combining marks: Hebrew niqqud/cantillation and Arabic
// harakat/Quranic annotation. Transparent for Arabic joining (do_shape
// skips them), zero-advance for measurement, and rendered as overlays on
// the preceding base glyph when the active font carries their glyphs.
// The cp >= 0x0591 guard keeps Latin combining marks (U+0300-U+036F, also
// NSM) on their existing utf8IsCombiningMark() rendering path.
return cp >= 0x0591 && bidi_class(cp) == NSM;
}
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) { bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) {
if (!utf8 || !*utf8) return false; if (!utf8 || !*utf8) return false;
const std::lock_guard<std::mutex> lock(bidiMutex);
static bidi_char line[BIDI_MAX_LINE]; static bidi_char line[BIDI_MAX_LINE];
static bidi_char shaped[BIDI_MAX_LINE];
int count = 0; int count = 0;
int lastBase = -1; // last non-formatter character (mintty's ibase)
uint8_t pendingJoiners = 0; // ZWJ/ZWNJ seen since lastBase
auto* p = reinterpret_cast<const unsigned char*>(utf8); auto* p = reinterpret_cast<const unsigned char*>(utf8);
while (*p) { while (*p) {
if (count >= BIDI_MAX_LINE) { if (count >= BIDI_MAX_LINE) {
@@ -84,18 +105,66 @@ bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) {
if (!cp || cp == REPLACEMENT_GLYPH) break; if (!cp || cp == REPLACEMENT_GLYPH) break;
line[count].origwc = line[count].wc = cp; line[count].origwc = line[count].wc = cp;
line[count].index = static_cast<uint16_t>(count); line[count].index = static_cast<uint16_t>(count);
line[count].joiners = 0;
// Flag Arabic joining formatters mintty-style (termline.c): the ZWJ/ZWNJ
// goes into the low nibble of the character it follows and the high
// nibble of the character it precedes. Flags are assigned in logical
// order here; do_shape() reads them after reordering.
if (cp == 0x200C || cp == 0x200D) {
const uint8_t joiner = (cp == 0x200D) ? ZWJ : ZWNJ;
if (lastBase >= 0) line[lastBase].joiners |= joiner;
pendingJoiners |= joiner;
} else {
line[count].joiners = pendingJoiners << 4;
pendingJoiners = 0;
lastBase = count;
}
count++; count++;
} }
if (!count) return false; if (!count) return false;
const bool autodir = (paragraphLevel < 0); const bool autodir = (paragraphLevel < 0);
const int level = autodir ? 0 : (paragraphLevel & 1); const int level = autodir ? 0 : (paragraphLevel & 1);
// Order matters (mintty does the same): do_bidi() first to obtain visual
// order, then do_shape() — contextual forms are resolved from *visual*
// adjacency, and shaping presentation forms must never be reordered.
do_bidi(autodir, level, line, count); do_bidi(autodir, level, line, count);
do_shape(line, shaped, count);
out.clear(); out.clear();
out.reserve(std::strlen(utf8)); out.reserve(std::strlen(utf8));
// Lam-Alef collapse sentinel and zero-width joining formatters have done
// their job during shaping and have no glyphs to render.
const auto filtered = [](const uint32_t cp) { return cp == LIGATURE_PLACEHOLDER || cp == 0x200C || cp == 0x200D; };
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
utf8AppendCodepoint(line[i].wc, out); const uint32_t cp = shaped[i].wc;
if (filtered(cp)) continue;
if (!isTransparentMark(cp)) {
utf8AppendCodepoint(cp, out);
continue;
}
// UAX#9 rule L3: reversing an RTL run leaves combining marks *before*
// their base character. The renderer overlays a mark on the most
// recently drawn glyph, so emit the base first, then its marks in
// logical order. `index` is the original logical position: a base
// following its marks with a *lower* index means the run was reversed.
int j = i; // [i, j) = the run of marks (and filtered entries)
while (j < count && (filtered(shaped[j].wc) || isTransparentMark(shaped[j].wc))) j++;
if (j < count && shaped[j].index < shaped[i].index) {
utf8AppendCodepoint(shaped[j].wc, out);
for (int k = j - 1; k >= i; k--) {
if (isTransparentMark(shaped[k].wc)) utf8AppendCodepoint(shaped[k].wc, out);
}
i = j; // base already emitted
} else {
// Unreversed (or trailing, base-less) marks already follow their base.
for (int k = i; k < j; k++) {
if (isTransparentMark(shaped[k].wc)) utf8AppendCodepoint(shaped[k].wc, out);
}
i = j - 1;
}
} }
return true; return true;
} }
@@ -105,6 +174,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
visualOrder.clear(); visualOrder.clear();
const size_t nWords = words.size(); const size_t nWords = words.size();
if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false; if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false;
const std::lock_guard<std::mutex> lock(bidiMutex);
static bidi_char line[BIDI_MAX_LINE]; static bidi_char line[BIDI_MAX_LINE];
int count = 0; int count = 0;
@@ -121,6 +191,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
if (!cp || cp == REPLACEMENT_GLYPH) break; if (!cp || cp == REPLACEMENT_GLYPH) break;
line[count].origwc = line[count].wc = cp; line[count].origwc = line[count].wc = cp;
line[count].index = static_cast<uint16_t>(w); line[count].index = static_cast<uint16_t>(w);
line[count].joiners = 0;
count++; count++;
} }
@@ -131,6 +202,7 @@ bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragrap
} }
line[count].origwc = line[count].wc = ' '; line[count].origwc = line[count].wc = ' ';
line[count].index = static_cast<uint16_t>(nWords); line[count].index = static_cast<uint16_t>(nWords);
line[count].joiners = 0;
count++; count++;
} }
} }
+8
View File
@@ -13,6 +13,14 @@ bool startsWithRtl(const char* utf8, int maxStrongChars = RTL_PARAGRAPH_PROBE_DE
int detectParagraphLevel(const char* utf8, int fallbackLevel = 0, int maxStrongChars = 64); int detectParagraphLevel(const char* utf8, int fallbackLevel = 0, int maxStrongChars = 64);
// True for RTL-script non-spacing marks (Hebrew niqqud/cantillation, Arabic
// harakat and Quranic annotation): zero-width for measurement, transparent for
// Arabic joining, and rendered as overlays on the preceding base glyph when
// the active font carries their glyphs (SD fonts; built-in fonts don't).
// Latin combining marks (U+0300-U+036F) intentionally return false — they are
// handled by the utf8IsCombiningMark() rendering path.
bool isTransparentMark(uint32_t cp);
// paragraphLevel: -1 = auto-detect, 0 = LTR, 1 = RTL // paragraphLevel: -1 = auto-detect, 0 = LTR, 1 = RTL
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel = -1); bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel = -1);
+48 -6
View File
@@ -1,15 +1,19 @@
/* bidiclasses.t — bidi class table for CrossPoint Hebrew/English epub. /* bidiclasses.t — bidi class table for CrossPoint RTL (Hebrew/Arabic) epub.
* *
* Coverage rationale: * Coverage rationale:
* Hebrew + English is the primary target. However, CrossPoint renders * Hebrew and Arabic-script languages (Arabic, Farsi, Urdu, Sindhi, Pashto,
* Latin and Cyrillic scripts for many other languages, so these MUST be * Kurdish) are the RTL targets. CrossPoint also renders Latin and Cyrillic
* classified as L (not fall through to ON) to avoid regression when they * scripts for many other languages, so these MUST be classified as L (not
* appear adjacent to Hebrew runs. * fall through to ON) to avoid regression when they appear adjacent to
* RTL runs.
* *
* Scripts NOT in this table fall through to ON correct per UAX#9 for * Scripts NOT in this table fall through to ON correct per UAX#9 for
* scripts CrossPoint's fonts don't support (CJK, Arabic, Devanagari, etc.) * scripts CrossPoint's fonts don't support (CJK, Devanagari, etc.)
* ON is the right class for "unknown" it behaves neutrally. * ON is the right class for "unknown" it behaves neutrally.
* *
* Arabic ranges are sourced from Unicode UCD extracted/DerivedBidiClass.txt
* (values verified against Unicode 17.0.0).
*
* Entries sorted ascending by first (binary search requirement). * Entries sorted ascending by first (binary search requirement).
*/ */
@@ -84,6 +88,36 @@
{0x05D0, 0x05EA, R}, /* alef … tav */ {0x05D0, 0x05EA, R}, /* alef … tav */
{0x05F0, 0x05F4, R}, /* alternative forms + geresh/gershayim */ {0x05F0, 0x05F4, R}, /* alternative forms + geresh/gershayim */
/* ── Arabic / Perso-Arabic (DerivedBidiClass.txt) */
/* Letters are AL (Arabic Letter), harakat/marks are NSM, Arabic-Indic
digits are AN, extended (Farsi/Urdu) digits are EN.
0x0606-0x0607, 0x060E-0x060F, 0x06DE, 0x06E9 are ON fall through. */
{0x0600, 0x0605, AN}, /* Arabic number signs (Cf) */
{0x0608, 0x0608, AL}, /* Arabic ray */
{0x0609, 0x060A, ET}, /* per mille / per ten thousand */
{0x060B, 0x060B, AL}, /* afghani sign */
{0x060C, 0x060C, CS}, /* Arabic comma */
{0x060D, 0x060D, AL}, /* Arabic date separator */
{0x0610, 0x061A, NSM}, /* honorifics + small marks */
{0x061B, 0x061F, AL}, /* semicolon, ALM, end-of-text, hamza mark, question mark */
{0x0620, 0x064A, AL}, /* core Arabic letters + tatweel */
{0x064B, 0x065F, NSM}, /* harakat (fathatan … wavy hamza below) */
{0x0660, 0x0669, AN}, /* Arabic-Indic digits ٠-٩ */
{0x066A, 0x066A, ET}, /* Arabic percent sign */
{0x066B, 0x066C, AN}, /* decimal / thousands separators */
{0x066D, 0x066F, AL}, /* five-pointed star, dotless beh/qaf */
{0x0670, 0x0670, NSM}, /* superscript alef */
{0x0671, 0x06D5, AL}, /* extended letters: Farsi, Urdu, Sindhi, Pashto, Kurdish */
{0x06D6, 0x06DC, NSM}, /* Quranic annotation marks */
{0x06DD, 0x06DD, AN}, /* end of ayah */
{0x06DF, 0x06E4, NSM},
{0x06E5, 0x06E6, AL}, /* small waw / small yeh */
{0x06E7, 0x06E8, NSM},
{0x06EA, 0x06ED, NSM},
{0x06EE, 0x06EF, AL}, /* dal/reh with inverted V */
{0x06F0, 0x06F9, EN}, /* extended Arabic-Indic digits ۰-۹ (Farsi/Urdu) EN per UCD */
{0x06FA, 0x06FF, AL},
/* ── Latin Extended Additional (L) ─────────────────────────────────── */ /* ── Latin Extended Additional (L) ─────────────────────────────────── */
/* Covers accented chars for Vietnamese, Welsh, Romanian, etc. /* Covers accented chars for Vietnamese, Welsh, Romanian, etc.
Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */ Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */
@@ -110,6 +144,14 @@
{0x2069, 0x2069, PDI}, {0x2069, 0x2069, PDI},
{0x206A, 0x206F, BN}, {0x206A, 0x206F, BN},
/* ── Arabic presentation forms (output of do_shape()) ───────────────── */
/* Contextual/ligature forms emitted by the shaper must classify as AL so
a reshaped line still resolves RTL. Ranges per DerivedBidiClass.txt;
0xFBC3-0xFBD2 are ON fall through. */
{0xFB50, 0xFBC2, AL}, /* Presentation Forms-A: Perso-Arabic contextual forms */
{0xFBD3, 0xFBFF, AL}, /* Presentation Forms-A: NG … Farsi Yeh forms */
{0xFE70, 0xFE74, AL}, /* Presentation Forms-B: harakat isolated forms */
{0xFE76, 0xFEFC, AL}, /* Presentation Forms-B: contextual forms + Lam-Alef ligatures */
/* ── Byte Order Mark ────────────────────────────────────────────────── */ /* ── Byte Order Mark ────────────────────────────────────────────────── */
{0xFEFF, 0xFEFF, BN}, {0xFEFF, 0xFEFF, BN},
+339
View File
@@ -127,6 +127,345 @@ ucschar mirror(ucschar c) {
return p ? p->to : c; return p ? p->to : c;
} }
/* ═══════════════════════════════════════════════════════════════════════
* Arabic contextual shaping — do_shape()
*
* Ported from mintty src/minibidi.c (https://github.com/mintty/mintty),
* original author Ahmad Khalifa (www.arabeyes.org), maintained by
* Thomas Wolff. MIT licence.
*
* CrossPoint deviations from the upstream code, each marked inline:
* 1. Joining-context lookups skip NSM marks (harakat). mintty stores
* combining marks out-of-line in terminal cells, so they never appear
* in its bidi_char array; in CrossPoint they are real array entries
* and must be transparent for joining (Unicode joining type T).
* 2. The Alef absorbed into a Lam-Alef ligature is overwritten with
* LIGATURE_PLACEHOLDER instead of a space: a terminal must keep the
* cell, a proportional-text renderer must drop the character.
* 3. The STYPE/SISOLATED macros became functions backed by a second
* lookup table covering Perso-Arabic letters outside mintty's native
* U+0621U+064A range (Farsi پ چ ژ گ, Urdu ٹ ڈ ڑ ں ہ ے, plus Sindhi/
* Pashto/Kurdish letters). Joining types are sourced from Unicode
* ArabicShaping.txt and presentation forms from UnicodeData.txt
* (Arabic Presentation Forms-A), both Unicode 17.0.0. Letters with a
* joining type but no presentation-form codepoints keep their base
* codepoint (neighbours still shape correctly around them).
* U+200C/U+200D also get their ArabicShaping.txt types (U and C) so
* that in-stream ZWJ/ZWNJ — which mintty never has in its array —
* affect adjacency the same way the joiners flags do.
* ═══════════════════════════════════════════════════════════════════════ */
/* Shaping Types (mintty) */
enum {
SL, /* Left-Joining, doesn't exist in U+0600 - U+06FF */
SR, /* Right-Joining, i.e. has Isolated, Final */
SD, /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */
SU, /* Non-Joining */
SC /* Join-Causing, like U+0640 (TATWEEL) */
};
typedef struct {
uchar type;
uchar form_b; /* isolated form = 0xFE00 + form_b (Presentation Forms-B) */
} shape_node;
/* Kept near the actual table, for verification. (mintty) */
enum { SHAPE_FIRST = 0x621, SHAPE_LAST = 0x64A };
/* mintty's shapetypes[] — verbatim */
static const shape_node shapetypes[] = {
/* index, Typ, Iso, Ligature Index */
/* 621 */ {SU, 0x80},
/* 622 */ {SR, 0x81},
/* 623 */ {SR, 0x83},
/* 624 */ {SR, 0x85},
/* 625 */ {SR, 0x87},
/* 626 */ {SD, 0x89},
/* 627 */ {SR, 0x8D},
/* 628 */ {SD, 0x8F},
/* 629 */ {SR, 0x93},
/* 62A */ {SD, 0x95},
/* 62B */ {SD, 0x99},
/* 62C */ {SD, 0x9D},
/* 62D */ {SD, 0xA1},
/* 62E */ {SD, 0xA5},
/* 62F */ {SR, 0xA9},
/* 630 */ {SR, 0xAB},
/* 631 */ {SR, 0xAD},
/* 632 */ {SR, 0xAF},
/* 633 */ {SD, 0xB1},
/* 634 */ {SD, 0xB5},
/* 635 */ {SD, 0xB9},
/* 636 */ {SD, 0xBD},
/* 637 */ {SD, 0xC1},
/* 638 */ {SD, 0xC5},
/* 639 */ {SD, 0xC9},
/* 63A */ {SD, 0xCD},
/* 63B */ {SU, 0x0},
/* 63C */ {SU, 0x0},
/* 63D */ {SU, 0x0},
/* 63E */ {SU, 0x0},
/* 63F */ {SU, 0x0},
/* 640 */ {SC, 0x0},
/* 641 */ {SD, 0xD1},
/* 642 */ {SD, 0xD5},
/* 643 */ {SD, 0xD9},
/* 644 */ {SD, 0xDD},
/* 645 */ {SD, 0xE1},
/* 646 */ {SD, 0xE5},
/* 647 */ {SD, 0xE9},
/* 648 */ {SR, 0xED},
/* 649 */ {SR, 0xEF},
/* 64A */ {SD, 0xF1}};
/* ── CrossPoint extension: joining types outside U+0621U+064A ──────────
* Source: Unicode ArabicShaping.txt (joining type column). Ranges of
* letters sharing one type are collapsed. Sorted ascending (binary
* search). Anything not listed here or in shapetypes[] is SU. */
static const struct {
ucschar first, last;
uchar type;
} xjointypes[] = {
{0x0620, 0x0620, SD}, /* kashmiri yeh */
{0x066E, 0x066F, SD}, /* dotless beh/qaf */
{0x0671, 0x0673, SR}, /* alef wasla + wavy-hamza alefs */
{0x0675, 0x0677, SR}, /* high-hamza alef/waw */
{0x0678, 0x0687, SD}, /* high-hamza yeh, beh group (ٹ ٺ ٻ … پ), hah group (چ ڇ) */
{0x0688, 0x0699, SR}, /* dal group (ڈ ڊ ڌ …), reh group (ڑ ړ ژ …) */
{0x069A, 0x06BF, SD}, /* seen/sad/feh/qaf/kaf/gaf/lam/noon groups (ک گ ں ھ …) */
{0x06C0, 0x06C0, SR}, /* heh with yeh above */
{0x06C1, 0x06C2, SD}, /* heh goal (ہ) */
{0x06C3, 0x06CB, SR}, /* teh marbuta goal, waw group (ۆ ۇ ۋ …) */
{0x06CC, 0x06CC, SD}, /* farsi yeh (ی) */
{0x06CD, 0x06CD, SR}, /* yeh with tail */
{0x06CE, 0x06CE, SD}, /* farsi yeh with V (Kurdish ێ) */
{0x06CF, 0x06CF, SR}, /* waw with dot above */
{0x06D0, 0x06D1, SD}, /* e (Pashto ې), yeh three dots below */
{0x06D2, 0x06D3, SR}, /* yeh barree (ے ۓ) */
{0x06D5, 0x06D5, SR}, /* ae (Kurdish ە) */
{0x06EE, 0x06EF, SR}, /* dal/reh with inverted V */
{0x06FA, 0x06FC, SD}, /* sheen/dad/ghain with dot below */
{0x06FF, 0x06FF, SD}, /* heh with inverted V */
{0x200C, 0x200C, SU}, /* ZWNJ — joining type U per ArabicShaping.txt */
{0x200D, 0x200D, SC}, /* ZWJ — joining type C per ArabicShaping.txt */
};
/* ── CrossPoint extension: presentation forms outside U+0621U+064A ─────
* Source: UnicodeData.txt, Arabic Presentation Forms-A (U+FB50U+FBFF).
* forms = number of consecutive presentation forms allocated for the
* letter, always in the order isolated, final, initial, medial (matching
* the SHAPE_* offsets below): 2 = isolated+final, 4 = all.
* Letters absent here have no presentation forms and keep their base
* codepoint. Sorted ascending (binary search). */
static const struct {
ucschar cp;
ucschar isolated;
uchar forms;
} xshapeforms[] = {
{0x0671, 0xFB50, 2}, /* alef wasla */
{0x0679, 0xFB66, 4}, /* tteh (Urdu ٹ) */
{0x067A, 0xFB5E, 4}, /* tteheh */
{0x067B, 0xFB52, 4}, /* beeh (Sindhi ٻ) */
{0x067E, 0xFB56, 4}, /* peh (Farsi پ) */
{0x067F, 0xFB62, 4}, /* teheh */
{0x0680, 0xFB5A, 4}, /* beheh (Sindhi ڀ) */
{0x0683, 0xFB76, 4}, /* nyeh (Sindhi ڃ) */
{0x0684, 0xFB72, 4}, /* dyeh (Sindhi ڄ) */
{0x0686, 0xFB7A, 4}, /* tcheh (Farsi چ) */
{0x0687, 0xFB7E, 4}, /* tcheheh (Sindhi ڇ) */
{0x0688, 0xFB88, 2}, /* ddal (Urdu ڈ) */
{0x068C, 0xFB84, 2}, /* dahal (Sindhi ڌ) */
{0x068D, 0xFB82, 2}, /* ddahal (Sindhi ڍ) */
{0x068E, 0xFB86, 2}, /* dul (Sindhi ڎ) */
{0x0691, 0xFB8C, 2}, /* rreh (Urdu ڑ) */
{0x0698, 0xFB8A, 2}, /* jeh (Farsi ژ) */
{0x06A4, 0xFB6A, 4}, /* veh (Kurdish ڤ) */
{0x06A6, 0xFB6E, 4}, /* peheh (Sindhi ڦ) */
{0x06A9, 0xFB8E, 4}, /* keheh (Farsi/Urdu ک) */
{0x06AD, 0xFBD3, 4}, /* ng */
{0x06AF, 0xFB92, 4}, /* gaf (Farsi/Urdu گ) */
{0x06B1, 0xFB9A, 4}, /* ngoeh (Sindhi ڱ) */
{0x06B3, 0xFB96, 4}, /* gueh (Sindhi ڳ) */
{0x06BA, 0xFB9E, 2}, /* noon ghunna (Urdu ں) — dual-joining but only
isolated+final forms exist; initial/medial
contexts keep the base codepoint */
{0x06BB, 0xFBA0, 4}, /* rnoon (Sindhi ڻ) */
{0x06BE, 0xFBAA, 4}, /* heh doachashmee (Urdu ھ) */
{0x06C0, 0xFBA4, 2}, /* heh with yeh above */
{0x06C1, 0xFBA6, 4}, /* heh goal (Urdu ہ) */
{0x06C5, 0xFBE0, 2}, /* kirghiz oe */
{0x06C6, 0xFBD9, 2}, /* oe (Kurdish ۆ) */
{0x06C7, 0xFBD7, 2}, /* u (ۇ) */
{0x06C8, 0xFBDB, 2}, /* yu */
{0x06C9, 0xFBE2, 2}, /* kirghiz yu */
{0x06CB, 0xFBDE, 2}, /* ve */
{0x06CC, 0xFBFC, 4}, /* farsi yeh (Farsi/Urdu ی) */
{0x06D0, 0xFBE4, 4}, /* e (Pashto ې) */
{0x06D2, 0xFBAE, 2}, /* yeh barree (Urdu ے) */
{0x06D3, 0xFBB0, 2}, /* yeh barree with hamza above (ۓ) */
};
/* Contextual form offsets from the isolated form — identical ordering in
Presentation Forms-A and -B, matching mintty's SFINAL/SINITIAL/SMEDIAL
(+1/+2/+3) macros. */
enum { SHAPE_ISOLATED = 0, SHAPE_FINAL = 1, SHAPE_INITIAL = 2, SHAPE_MEDIAL = 3 };
/* STYPE equivalent (mintty macro → function to add the extended table) */
static uchar stype(ucschar c) {
if (c >= SHAPE_FIRST && c <= SHAPE_LAST) return shapetypes[c - SHAPE_FIRST].type;
int i = -1, j = lengthof(xjointypes);
while (j - i > 1) {
int k = (i + j) / 2;
if (c < xjointypes[k].first)
j = k;
else if (c > xjointypes[k].last)
i = k;
else
return xjointypes[k].type;
}
return SU;
}
/* SISOLATED/SFINAL/SINITIAL/SMEDIAL equivalent.
form is one of the SHAPE_* offsets; returns c unchanged when the letter
has no presentation form allocated for that context. */
static ucschar shape_form(ucschar c, uchar form) {
if (c >= SHAPE_FIRST && c <= SHAPE_LAST) {
const uchar form_b = shapetypes[c - SHAPE_FIRST].form_b;
return form_b ? 0xFE00 + form_b + form : c;
}
int i = -1, j = lengthof(xshapeforms);
while (j - i > 1) {
int k = (i + j) / 2;
if (c < xshapeforms[k].cp)
j = k;
else if (c > xshapeforms[k].cp)
i = k;
else
return form < xshapeforms[k].forms ? xshapeforms[k].isolated + form : c;
}
return c;
}
/* CrossPoint deviation 1: NSM marks (harakat) sit between letters in our
array but are transparent for joining (Unicode joining type T). These
helpers find the effective joining neighbour. */
static int next_non_nsm(const bidi_char* line, int i, int count) {
int j = i + 1;
while (j < count && bidi_class(line[j].wc) == NSM) j++;
return j;
}
static int prev_non_nsm(const bidi_char* line, int i) {
int j = i - 1;
while (j >= 0 && bidi_class(line[j].wc) == NSM) j--;
return j;
}
/* The Main shaping function (mintty, structure preserved).
*
* line: visual-order buffer — must have been passed through do_bidi() first
* to: output buffer for the shaped data
* count: number of characters in line
*/
int do_shape(bidi_char* line, bidi_char* to, int count) {
for (int i = 0; i < count; i++) {
to[i] = line[i];
int tempShape = stype(line[i].wc);
switch (tempShape) {
case SR: { /* Right-Joining, i.e. has Isolated, Final */
const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */
tempShape = (nx < count) ? stype(line[nx].wc) : SU;
if (tempShape == SL || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
break;
}
case SD: { /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */
const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */
const int pv = prev_non_nsm(line, i); /* deviation 1: was i - 1 */
/* Make Ligatures */
tempShape = (nx < count) ? stype(line[nx].wc) : SU;
if (line[i].wc == 0x644) { /* Lam: the Alef variant is at pv (visually left) */
int ligFlag = 0;
switch (pv >= 0 ? line[pv].wc : 0) {
case 0x622: /* Alef with Madda above → لآ */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF6 : 0xFEF5;
break;
case 0x623: /* Alef with Hamza above → لأ */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF8 : 0xFEF7;
break;
case 0x625: /* Alef with Hamza below → لإ */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFA : 0xFEF9;
break;
case 0x627: /* Alef → لا */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFC : 0xFEFB;
break;
}
if (ligFlag) {
to[pv].wc = LIGATURE_PLACEHOLDER; /* deviation 2: mintty writes 0x20 */
break;
}
}
/* Arabic joining formatters: adapt forms (mintty) */
const uchar joiners = line[i].joiners & 0xF;
const uchar prevjoiners = line[i].joiners >> 4;
if (prevjoiners == ZWNJ) {
/* backward join blocked; initial only if the visually-right
(logically next) neighbour joins, else isolated */
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
if (tempShape == SR || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
} else if (prevjoiners == (ZWJ | ZWNJ)) {
to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL);
} else if (prevjoiners == ZWJ) {
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
} else if (joiners & ZWNJ) {
/* forward join blocked; final only if the visually-left
(logically previous) neighbour joins, else isolated —
tempShape still holds stype(nx) from above */
if (tempShape == SL || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
} else if (tempShape == SL || tempShape == SD || tempShape == SC) {
/* visually-right neighbour joins → final, or medial if the
visually-left neighbour joins too */
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
if (tempShape == SR || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
} else {
/* visually-right neighbour doesn't join → isolated, or initial
if the visually-left neighbour joins */
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
if (tempShape == SR || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
}
break;
}
default:
/* SU, SL, SC and non-Arabic characters pass through unchanged */
break;
}
}
return 1;
}
/* ═══════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════
* Directional Status Stack * Directional Status Stack
* (replaces GCC nested functions — ESP32C3 has no executable stack) * (replaces GCC nested functions — ESP32C3 has no executable stack)
+52 -5
View File
@@ -5,8 +5,10 @@
* minibidi.h — standalone header for ESP32C3 BiDi calculations * minibidi.h — standalone header for ESP32C3 BiDi calculations
* *
* Derived from [mintty](https://github.com/mintty/mintty/) (Thomas Wolff, MIT licence). * Derived from [mintty](https://github.com/mintty/mintty/) (Thomas Wolff, MIT licence).
* Stripped of: Arabic shaping, box-drawing mirror, terminal dependencies, * Includes: UAX#9 bidi (do_bidi) and Arabic contextual shaping (do_shape),
* GCC nested functions, VLAs, and non-Hebrew/English Unicode data. * both ported from mintty src/minibidi.c (Ahmad Khalifa, Thomas Wolff).
* Stripped of: box-drawing mirror, terminal dependencies, GCC nested
* functions, VLAs, and Unicode data for scripts CrossPoint doesn't render.
*/ */
#include <stdbool.h> #include <stdbool.h>
@@ -34,13 +36,18 @@ typedef uint32_t ucschar; /* Unicode codepoint; BMP-only content fits uint16_t
#define BIDI_MAX_LINE 128 #define BIDI_MAX_LINE 128
/* ── bidi_char ───────────────────────────────────────────────────────── */ /* ── bidi_char ───────────────────────────────────────────────────────── */
/* origwc: the codepoint as it came from the epub text stream /* origwc: the codepoint as it came from the epub text stream
wc: working codepoint (may be replaced by mirrored form after do_bidi) wc: working codepoint (may be replaced by mirrored form after
index: original logical position, so the caller can reorder glyphs */ do_bidi, or by an Arabic contextual form after do_shape)
index: original logical position, so the caller can reorder glyphs
joiners: ZWJ/ZWNJ context for Arabic shaping, mintty layout:
low nibble = joiners that logically FOLLOW this character,
high nibble = joiners that logically PRECEDE this character */
typedef struct { typedef struct {
ucschar origwc; ucschar origwc;
ucschar wc; ucschar wc;
uint16_t index; uint16_t index;
uint8_t joiners;
} bidi_char; } bidi_char;
/* ── Bidi character classes (UAX #9) ────────────────────────────────── */ /* ── Bidi character classes (UAX #9) ────────────────────────────────── */
@@ -71,6 +78,20 @@ enum {
PDI, /* Pop Directional Isolate */ PDI, /* Pop Directional Isolate */
}; };
/* ── Arabic joining formatter flags (bidi_char.joiners nibbles) ─────── */
/* Values match mintty's minibidi.h: ZWNJ 0x01, ZWJ 0x02. do_shape()
compares nibble values directly, so these must not be changed. */
enum {
ZWNJ = 0x01, /* U+200C ZERO WIDTH NON-JOINER */
ZWJ = 0x02, /* U+200D ZERO WIDTH JOINER */
};
/* Sentinel written by do_shape() over the Alef absorbed into a Lam-Alef
ligature. Callers must filter it out when emitting shaped text.
(Upstream mintty writes a space instead — a terminal must keep the cell;
a proportional-text renderer must drop the character entirely.) */
#define LIGATURE_PLACEHOLDER 0xFFFFu
/* ── Public API ──────────────────────────────────────────────────────── */ /* ── Public API ──────────────────────────────────────────────────────── */
/* /*
@@ -94,6 +115,32 @@ bool is_rtl_class(uchar bc);
*/ */
ucschar mirror(ucschar ch); ucschar mirror(ucschar ch);
/*
* do_shape(line, to, count)
*
* Applies Arabic contextual shaping (and Lam-Alef ligation) to
* `line[0..count-1]`, writing the result to `to[0..count-1]`.
*
* MUST be called AFTER do_bidi(): the algorithm resolves joining from
* VISUAL adjacency (line[i-1] is the visually-left neighbour, line[i+1]
* the visually-right one), exactly like upstream mintty.
*
* line: visual-order input; the joiners field must be populated by the
* caller (in logical order, before do_bidi) for ZWJ/ZWNJ support
* to: output buffer, same size as line; non-Arabic entries are copied
* through unchanged. An Alef absorbed by a Lam-Alef ligature is
* replaced with LIGATURE_PLACEHOLDER — filter it on emission.
* count: number of characters (≤ BIDI_MAX_LINE)
*
* Returns 1.
*
* Ported from mintty src/minibidi.c (Ahmad Khalifa, Thomas Wolff,
* MIT licence), https://github.com/mintty/mintty — with CrossPoint
* extensions for Perso-Arabic letters and in-stream diacritics, see
* minibidi.c for details.
*/
int do_shape(bidi_char* line, bidi_char* to, int count);
/* /*
* do_bidi(autodir, paragraphLevel, line, count) * do_bidi(autodir, paragraphLevel, line, count)
* *
+55 -18
View File
@@ -5,6 +5,17 @@
#include <cstring> #include <cstring>
namespace {
constexpr size_t ENTRY_STORAGE_CAPACITY = 64;
constexpr size_t MAX_ENTRIES = ENTRY_STORAGE_CAPACITY - 2;
constexpr size_t MAX_TITLE_CHARS = 160;
constexpr size_t MAX_AUTHOR_CHARS = 120;
constexpr size_t MAX_ID_CHARS = 128;
constexpr size_t MAX_HREF_CHARS = 768;
constexpr size_t MAX_SEARCH_TEMPLATE_CHARS = 768;
constexpr size_t MAX_PAGE_URL_CHARS = 768;
} // namespace
OpdsParser::OpdsParser() { OpdsParser::OpdsParser() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
@@ -12,6 +23,7 @@ OpdsParser::OpdsParser() {
LOG_DBG("OPDS", "Couldn't allocate memory for parser"); LOG_DBG("OPDS", "Couldn't allocate memory for parser");
return; return;
} }
entries.reserve(ENTRY_STORAGE_CAPACITY);
XML_SetUserData(parser, this); XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement); XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData); XML_SetCharacterDataHandler(parser, characterData);
@@ -71,6 +83,8 @@ void OpdsParser::clear() {
currentEntry = OpdsEntry{}; currentEntry = OpdsEntry{};
currentText.clear(); currentText.clear();
inEntry = inTitle = inAuthor = inAuthorName = inId = false; inEntry = inTitle = inAuthor = inAuthorName = inId = false;
collectCurrentEntry = false;
feedTruncated = false;
} }
std::vector<OpdsEntry> OpdsParser::getBooks() const { std::vector<OpdsEntry> OpdsParser::getBooks() const {
@@ -88,9 +102,33 @@ const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
return nullptr; return nullptr;
} }
void OpdsParser::assignBounded(std::string& target, const char* value, const size_t maxLen) {
if (!value) {
target.clear();
return;
}
target.assign(value, strnlen(value, maxLen));
}
void OpdsParser::appendBounded(std::string& target, const char* value, const size_t len, const size_t maxLen) {
if (target.size() >= maxLen) return;
const size_t remaining = maxLen - target.size();
target.append(value, len < remaining ? len : remaining);
}
void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<OpdsParser*>(userData); auto* self = static_cast<OpdsParser*>(userData);
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
self->inEntry = true;
self->collectCurrentEntry = self->entries.size() < MAX_ENTRIES;
self->feedTruncated = self->feedTruncated || !self->collectCurrentEntry;
self->currentEntry = OpdsEntry{};
self->currentText.clear();
self->inTitle = self->inAuthor = self->inAuthorName = self->inId = false;
return;
}
if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) { if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) {
const char* href = findAttribute(atts, "href"); const char* href = findAttribute(atts, "href");
if (href) { if (href) {
@@ -98,17 +136,16 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
const char* type = findAttribute(atts, "type"); const char* type = findAttribute(atts, "type");
if (rel && strcmp(rel, "search") == 0) { if (rel && strcmp(rel, "search") == 0) {
std::string sHref(href); if (strstr(href, "{searchTerms}") != nullptr) {
if (sHref.find("{searchTerms}") != std::string::npos) { assignBounded(self->searchTemplate, href, MAX_SEARCH_TEMPLATE_CHARS);
self->searchTemplate = sHref;
} }
} else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) { } else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) {
self->nextPageUrl = href; assignBounded(self->nextPageUrl, href, MAX_PAGE_URL_CHARS);
} else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) { } else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) {
self->prevPageUrl = href; assignBounded(self->prevPageUrl, href, MAX_PAGE_URL_CHARS);
} }
if (self->inEntry) { if (self->inEntry && self->collectCurrentEntry) {
if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr &&
strcmp(type, "application/epub+zip") == 0) { strcmp(type, "application/epub+zip") == 0) {
// Prefer plain EPUB links over derived formats when multiple // Prefer plain EPUB links over derived formats when multiple
@@ -119,25 +156,19 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
self->currentEntry.href.find("/epub/") != std::string::npos); self->currentEntry.href.find("/epub/") != std::string::npos);
if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) { if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) {
self->currentEntry.type = OpdsEntryType::BOOK; self->currentEntry.type = OpdsEntryType::BOOK;
self->currentEntry.href = href; assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS);
} }
} else if (type && strstr(type, "application/atom+xml") != nullptr) { } else if (type && strstr(type, "application/atom+xml") != nullptr) {
if (self->currentEntry.type != OpdsEntryType::BOOK) { if (self->currentEntry.type != OpdsEntryType::BOOK) {
self->currentEntry.type = OpdsEntryType::NAVIGATION; self->currentEntry.type = OpdsEntryType::NAVIGATION;
self->currentEntry.href = href; assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS);
} }
} }
} }
} }
} }
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { if (!self->inEntry || !self->collectCurrentEntry) return;
self->inEntry = true;
self->currentEntry = OpdsEntry{};
return;
}
if (!self->inEntry) return;
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
self->inTitle = true; self->inTitle = true;
@@ -157,10 +188,11 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<OpdsParser*>(userData); auto* self = static_cast<OpdsParser*>(userData);
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) { if (self->collectCurrentEntry && !self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
self->entries.push_back(self->currentEntry); self->entries.push_back(self->currentEntry);
} }
self->inEntry = false; self->inEntry = false;
self->collectCurrentEntry = false;
} else if (self->inEntry) { } else if (self->inEntry) {
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
if (self->inTitle) self->currentEntry.title = self->currentText; if (self->inTitle) self->currentEntry.title = self->currentText;
@@ -179,7 +211,12 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) { void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<OpdsParser*>(userData); auto* self = static_cast<OpdsParser*>(userData);
if (self->inTitle || self->inAuthorName || self->inId) { if (!self->collectCurrentEntry) return;
self->currentText.append(s, len); if (self->inTitle) {
appendBounded(self->currentText, s, len, MAX_TITLE_CHARS);
} else if (self->inAuthorName) {
appendBounded(self->currentText, s, len, MAX_AUTHOR_CHARS);
} else if (self->inId) {
appendBounded(self->currentText, s, len, MAX_ID_CHARS);
} }
} }
+5
View File
@@ -61,6 +61,7 @@ class OpdsParser final : public Print {
void flush() override; void flush() override;
bool error() const; bool error() const;
bool truncated() const { return feedTruncated; }
operator bool() { return !error(); } operator bool() { return !error(); }
@@ -93,6 +94,8 @@ class OpdsParser final : public Print {
std::string prevPageUrl; std::string prevPageUrl;
// Helper to find attribute value // Helper to find attribute value
static const char* findAttribute(const XML_Char** atts, const char* name); static const char* findAttribute(const XML_Char** atts, const char* name);
static void assignBounded(std::string& target, const char* value, size_t maxLen);
static void appendBounded(std::string& target, const char* value, size_t len, size_t maxLen);
XML_Parser parser = nullptr; XML_Parser parser = nullptr;
std::vector<OpdsEntry> entries; std::vector<OpdsEntry> entries;
@@ -105,6 +108,8 @@ class OpdsParser final : public Print {
bool inAuthor = false; bool inAuthor = false;
bool inAuthorName = false; bool inAuthorName = false;
bool inId = false; bool inId = false;
bool collectCurrentEntry = false;
bool errorOccured = false; bool errorOccured = false;
bool feedTruncated = false;
}; };
+18 -22
View File
@@ -2,7 +2,7 @@
#include <HalDisplay.h> #include <HalDisplay.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <InflateReader.h> #include <InflateStream.h>
#include <Logging.h> #include <Logging.h>
#include <cstdio> #include <cstdio>
@@ -174,9 +174,8 @@ void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
} // namespace } // namespace
// Context for streaming PNG decompression // Context for streaming PNG decompression
// IMPORTANT: reader must be the first field - the uzlib callback casts uzlib_uncomp* to PngDecodeContext*
struct PngDecodeContext { struct PngDecodeContext {
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to PngDecodeContext* InflateStream reader;
HalFile* file; HalFile* file;
// PNG image properties // PNG image properties
@@ -195,7 +194,7 @@ struct PngDecodeContext {
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
bool idatFinished; // no more IDAT chunks bool idatFinished; // no more IDAT chunks
// File read buffer for feeding uzlib // File read buffer for feeding the inflate stream
uint8_t readBuf[2048]; uint8_t readBuf[2048];
// Palette for indexed color (type 3) // Palette for indexed color (type 3)
@@ -229,21 +228,21 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) {
} }
} }
// uzlib callback: reads the next batch of IDAT data from the file // Fill callback: reads the next batch of IDAT data from the file
static int pngIdatReadCallback(uzlib_uncomp* uncomp) { static size_t pngIdatFillCallback(void* vctx, const uint8_t** data) {
auto* ctx = reinterpret_cast<PngDecodeContext*>(uncomp); auto* ctx = static_cast<PngDecodeContext*>(vctx);
if (ctx->idatFinished) return -1; if (ctx->idatFinished) return 0;
// Skip 4-byte CRC and find next IDAT chunk when current chunk is exhausted // Skip 4-byte CRC and find next IDAT chunk when current chunk is exhausted
while (ctx->chunkBytesRemaining == 0) { while (ctx->chunkBytesRemaining == 0) {
if (!ctx->file->seekCur(4)) { // skip 4-byte CRC of previous IDAT if (!ctx->file->seekCur(4)) { // skip 4-byte CRC of previous IDAT
ctx->idatFinished = true; ctx->idatFinished = true;
return -1; return 0;
} }
if (!findNextIdatChunk(*ctx)) { if (!findNextIdatChunk(*ctx)) {
ctx->idatFinished = true; ctx->idatFinished = true;
return -1; return 0;
} }
} }
@@ -251,18 +250,15 @@ static int pngIdatReadCallback(uzlib_uncomp* uncomp) {
size_t toRead = sizeof(ctx->readBuf); size_t toRead = sizeof(ctx->readBuf);
if (toRead > ctx->chunkBytesRemaining) toRead = ctx->chunkBytesRemaining; if (toRead > ctx->chunkBytesRemaining) toRead = ctx->chunkBytesRemaining;
int bytesRead = ctx->file->read(ctx->readBuf, toRead); const int bytesRead = ctx->file->read(ctx->readBuf, toRead);
if (bytesRead <= 0) { if (bytesRead <= 0) {
ctx->idatFinished = true; ctx->idatFinished = true;
return -1; return 0;
} }
ctx->chunkBytesRemaining -= bytesRead; ctx->chunkBytesRemaining -= bytesRead;
*data = ctx->readBuf;
// Give uzlib the buffer (skip first byte since we return it directly) return static_cast<size_t>(bytesRead);
uncomp->source = ctx->readBuf + 1;
uncomp->source_limit = ctx->readBuf + bytesRead;
return ctx->readBuf[0];
} }
// Decode one scanline: decompress filter byte + raw bytes, then unfilter // Decode one scanline: decompress filter byte + raw bytes, then unfilter
@@ -555,16 +551,16 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(HalFile& pngFile, Print& bmpO
return false; return false;
} }
// Initialize streaming decompressor with 32KB ring buffer for back-reference history // Initialize streaming decompressor with 32KB window for back-reference history
if (!ctx.reader.init(true)) { if (!ctx.reader.init(true)) {
LOG_ERR("PNG", "Failed to init inflate reader"); LOG_ERR("PNG", "Failed to init inflate stream");
free(ctx.currentRow); free(ctx.currentRow);
free(ctx.previousRow); free(ctx.previousRow);
return false; return false;
} }
ctx.reader.setReadCallback(pngIdatReadCallback); ctx.reader.setFill(pngIdatFillCallback, &ctx);
// PNG IDAT data is zlib-wrapped: consume the 2-byte zlib header (CMF + FLG) // PNG IDAT data is zlib-wrapped (2-byte header + trailing adler32)
ctx.reader.skipZlibHeader(); ctx.reader.setZlibWrapped();
// Calculate output dimensions (same logic as JpegToBmpConverter) // Calculate output dimensions (same logic as JpegToBmpConverter)
int outWidth = width; int outWidth = width;
+161
View File
@@ -0,0 +1,161 @@
#pragma once
#include <HalStorage.h>
#include <Memory.h>
#include <algorithm>
#include <cstring>
#include <string>
namespace serialization {
// Sequential buffered wrappers over HalFile.
//
// SdFat keeps ONE shared 512-byte sector cache per volume, so interleaving small
// reads/writes across two or more files evicts and reloads that sector on nearly
// every call -- each 4-byte pod becomes a full SD transaction (measured: 31s to
// stream ~200KB through BookMetadataCache::buildBookBin on a 1,732-spine EPUB).
// Batching into chunk-sized transfers keeps each file at sequential SD speed.
//
// Heap: one fixed buffer per wrapper, allocated once at construction and freed at
// scope exit. If the allocation fails the wrapper degrades to unbuffered
// passthrough -- correct, just slow -- so callers never need an OOM path.
//
// Constraint: the wrapper must be the file's ONLY accessor while alive (it tracks
// the underlying position itself); mixing direct HalFile calls in desynchronizes it.
class BufferedFileWriter {
public:
BufferedFileWriter(HalFile& file, const size_t capacity)
: file(file), buf(makeUniqueNoThrow<uint8_t[]>(capacity)), cap(buf ? capacity : 0), pos(file.position()) {}
~BufferedFileWriter() { flush(); }
BufferedFileWriter(const BufferedFileWriter&) = delete;
BufferedFileWriter& operator=(const BufferedFileWriter&) = delete;
void write(const void* src, const size_t len) {
pos += len;
const auto* p = static_cast<const uint8_t*>(src);
if (fill + len > cap) {
flushBuffer();
}
if (len >= cap) { // also the cap == 0 passthrough
okFlag &= file.write(p, len) == len;
return;
}
// Typed local: cppcheck misreads unique_ptr<uint8_t[]>::get() arithmetic as void*.
uint8_t* const data = buf.get();
memcpy(data + fill, p, len);
fill += len;
}
// Logical write position (bytes written since the file was opened).
size_t position() const { return pos; }
// Flush buffered bytes; returns false if any write so far has failed short.
bool flush() {
flushBuffer();
return okFlag;
}
private:
void flushBuffer() {
if (fill == 0) return;
okFlag &= file.write(buf.get(), fill) == fill;
fill = 0;
}
HalFile& file;
std::unique_ptr<uint8_t[]> buf;
const size_t cap;
size_t fill = 0;
size_t pos;
bool okFlag = true;
};
class BufferedFileReader {
public:
BufferedFileReader(HalFile& file, const size_t capacity)
: file(file), buf(makeUniqueNoThrow<uint8_t[]>(capacity)), cap(buf ? capacity : 0), bufStart(file.position()) {}
BufferedFileReader(const BufferedFileReader&) = delete;
BufferedFileReader& operator=(const BufferedFileReader&) = delete;
size_t read(void* dst, size_t len) {
auto* p = static_cast<uint8_t*>(dst);
if (cap == 0) { // passthrough
const int n = file.read(p, len);
const size_t got = n < 0 ? 0 : static_cast<size_t>(n);
bufStart += got;
return got;
}
size_t total = 0;
while (len > 0) {
if (off == fill) {
bufStart += fill;
off = 0;
const int n = file.read(buf.get(), cap);
fill = n < 0 ? 0 : static_cast<size_t>(n);
if (fill == 0) break; // EOF or error
}
const size_t chunk = std::min(len, fill - off);
// Typed local: cppcheck misreads unique_ptr<uint8_t[]>::get() arithmetic as void*.
const uint8_t* const data = buf.get();
memcpy(p, data + off, chunk);
p += chunk;
off += chunk;
len -= chunk;
total += chunk;
}
return total;
}
// Logical read position.
size_t position() const { return bufStart + off; }
bool seek(const size_t target) {
// Within the buffered window: just move the cursor.
if (cap != 0 && target >= bufStart && target < bufStart + fill) {
off = target - bufStart;
return true;
}
if (!file.seek(target)) return false;
bufStart = target;
fill = 0;
off = 0;
return true;
}
private:
HalFile& file;
std::unique_ptr<uint8_t[]> buf;
const size_t cap;
size_t fill = 0;
size_t off = 0;
size_t bufStart;
};
// serialization:: overloads mirroring the HalFile ones in Serialization.h.
template <typename T>
void writePod(BufferedFileWriter& out, const T& value) {
out.write(&value, sizeof(T));
}
template <typename T>
void readPod(BufferedFileReader& in, T& value) {
in.read(&value, sizeof(T));
}
inline void writeString(BufferedFileWriter& out, const std::string& s) {
const uint32_t len = s.size();
writePod(out, len);
out.write(s.data(), len);
}
inline void readString(BufferedFileReader& in, std::string& s) {
uint32_t len;
readPod(in, len);
s.resize(len);
if (len > 0) {
in.read(&s[0], len);
}
}
} // namespace serialization
+45
View File
@@ -0,0 +1,45 @@
#include "PersistableStore.h"
#include <HalStorage.h>
#include <Logging.h>
#include <ObfuscationUtils.h>
bool PersistableStoreBase::writeDocToFile(const char* path, const JsonDocument& doc) {
Storage.mkdir("/.crosspoint");
String json;
serializeJson(doc, json);
if (!Storage.writeFile(path, json)) {
LOG_ERR("PERSIST", "Failed to write %s", path);
return false;
}
return true;
}
bool PersistableStoreBase::readDocFromFile(const char* path, JsonDocument& doc) {
if (!Storage.exists(path)) {
return false; // Expected on first boot — not an error.
}
String json = Storage.readFile(path);
if (json.isEmpty()) {
LOG_ERR("PERSIST", "Failed to read %s (empty)", path);
return false;
}
auto error = deserializeJson(doc, json);
if (error) {
LOG_ERR("PERSIST", "JSON parse error in %s: %s", path, error.c_str());
return false;
}
return true;
}
std::string PersistableStoreBase::extractPassword(JsonVariantConst doc, bool& needsResave) {
bool ok = false;
std::string pass = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
if (!ok) {
// Deobfuscation failed — fall back to legacy plaintext password.
pass = doc["password"] | "";
if (!pass.empty()) needsResave = true;
}
// A successfully decoded empty string is a legitimate value; preserve as-is.
return pass;
}
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <string>
/**
* @brief Non-template core of PersistableStore.
*
* All ArduinoJson parse/serialize machinery is instantiated once here (in
* PersistableStore.cpp) instead of in every store's translation unit. GCC
* emits the JSON serializer/parser templates as local .isra clones per TU
* (~0.5KB each), so keeping serializeJson/deserializeJson out of the stores
* is what makes the abstraction flash-neutral.
*/
class PersistableStoreBase {
protected:
PersistableStoreBase() = default;
~PersistableStoreBase() = default;
// Serializes doc and writes it to path (ensures /.crosspoint exists). Logs on failure.
static bool writeDocToFile(const char* path, const JsonDocument& doc);
// Reads path and parses it into doc. Returns false silently when the file
// does not exist (expected on first boot); logs on read/parse failure.
static bool readDocFromFile(const char* path, JsonDocument& doc);
/**
* Helper function for extracting an obfuscated password from a JSON value.
* Accepts JsonVariantConst so callers can pass either a whole JsonDocument
* or a JsonObject element (e.g. inside an array iteration).
* If the decoded password requires a resave (e.g. from plaintext fallback), `needsResave` is set to true.
*/
static std::string extractPassword(JsonVariantConst doc, bool& needsResave);
};
/**
* @brief Base class for persistable singletons using CRTP.
*
* Derived classes must provide:
* - A private default constructor
* - friend class PersistableStore<Derived>;
* - static const char* getFilePath();
* - void toJson(JsonDocument& doc) const;
* - bool fromJson(JsonVariantConst doc);
*
* Note for implementers: read string values as `const char*` (e.g.
* `obj["name"] | ""`), never as `| std::string("")` — ArduinoJson's
* std::string converter drags a per-TU copy of the whole JSON serializer
* into flash via its serializeJson fallback.
*/
template <typename T>
class PersistableStore : public PersistableStoreBase {
protected:
PersistableStore() = default;
~PersistableStore() = default;
public:
// Delete copy constructor and assignment
PersistableStore(const PersistableStore&) = delete;
PersistableStore& operator=(const PersistableStore&) = delete;
static T& getInstance() {
static T instance;
return instance;
}
bool saveToFile() const {
JsonDocument doc;
static_cast<const T*>(this)->toJson(doc);
return writeDocToFile(T::getFilePath(), doc);
}
bool loadFromFile() {
JsonDocument doc;
if (!readDocFromFile(T::getFilePath(), doc)) {
return false;
}
return static_cast<T*>(this)->fromJson(doc.as<JsonVariantConst>());
}
};
+22 -22
View File
@@ -1,13 +1,12 @@
#include "ZipFile.h" #include "ZipFile.h"
#include <HalStorage.h> #include <HalStorage.h>
#include <InflateReader.h> #include <InflateStream.h>
#include <Logging.h> #include <Logging.h>
#include <algorithm> #include <algorithm>
struct ZipInflateCtx { struct ZipInflateCtx {
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx*
HalFile* file = nullptr; HalFile* file = nullptr;
size_t fileRemaining = 0; size_t fileRemaining = 0;
uint8_t* readBuf = nullptr; uint8_t* readBuf = nullptr;
@@ -40,19 +39,16 @@ class ScopedOpenClose final {
bool ok = true; // true when zip was already open (no open() call needed) bool ok = true; // true when zip was already open (no open() call needed)
}; };
int zipReadCallback(uzlib_uncomp* uncomp) { size_t zipFillCallback(void* vctx, const uint8_t** data) {
auto* ctx = reinterpret_cast<ZipInflateCtx*>(uncomp); auto* ctx = static_cast<ZipInflateCtx*>(vctx);
if (ctx->fileRemaining == 0) return -1; if (ctx->fileRemaining == 0) return 0;
const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize; const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize;
const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead); const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead);
ctx->fileRemaining -= bytesRead; ctx->fileRemaining -= bytesRead;
if (bytesRead == 0) return -1; *data = ctx->readBuf;
return bytesRead;
uncomp->source = ctx->readBuf + 1;
uncomp->source_limit = ctx->readBuf + bytesRead;
return ctx->readBuf[0];
} }
} // namespace } // namespace
@@ -410,15 +406,18 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
ctx.readBuf = fileReadBuffer; ctx.readBuf = fileReadBuffer;
ctx.readBufSize = 1024; ctx.readBufSize = 1024;
if (!ctx.reader.init(true)) { // One-shot mode: `data` holds the entire output, so back-references
LOG_ERR("ZIP", "Failed to init inflate reader"); // resolve inside it and no 32KB window is allocated.
InflateStream inflate;
if (!inflate.init(false)) {
LOG_ERR("ZIP", "Failed to init inflate stream");
free(fileReadBuffer); free(fileReadBuffer);
free(data); free(data);
return nullptr; return nullptr;
} }
ctx.reader.setReadCallback(zipReadCallback); inflate.setFill(zipFillCallback, &ctx);
if (!ctx.reader.read(data, inflatedDataSize)) { if (!inflate.read(data, inflatedDataSize)) {
LOG_ERR("ZIP", "Failed to inflate file"); LOG_ERR("ZIP", "Failed to inflate file");
free(fileReadBuffer); free(fileReadBuffer);
free(data); free(data);
@@ -501,20 +500,21 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
ctx.readBuf = fileReadBuffer; ctx.readBuf = fileReadBuffer;
ctx.readBufSize = chunkSize; ctx.readBufSize = chunkSize;
if (!ctx.reader.init(true)) { InflateStream inflate;
LOG_ERR("ZIP", "Failed to init inflate reader"); if (!inflate.init(true)) {
LOG_ERR("ZIP", "Failed to init inflate stream");
free(outputBuffer); free(outputBuffer);
free(fileReadBuffer); free(fileReadBuffer);
return false; return false;
} }
ctx.reader.setReadCallback(zipReadCallback); inflate.setFill(zipFillCallback, &ctx);
bool success = false; bool success = false;
size_t totalProduced = 0; size_t totalProduced = 0;
while (true) { while (true) {
size_t produced; size_t produced;
const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced); const InflateStream::Status status = inflate.readAtMost(outputBuffer, chunkSize, &produced);
totalProduced += produced; totalProduced += produced;
if (totalProduced > static_cast<size_t>(inflatedDataSize)) { if (totalProduced > static_cast<size_t>(inflatedDataSize)) {
@@ -530,7 +530,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
} }
} }
if (status == InflateStatus::Done) { if (status == InflateStream::Status::Done) {
if (totalProduced != static_cast<size_t>(inflatedDataSize)) { if (totalProduced != static_cast<size_t>(inflatedDataSize)) {
LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize), LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize),
totalProduced); totalProduced);
@@ -541,16 +541,16 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
break; break;
} }
if (status == InflateStatus::Error) { if (status == InflateStream::Status::Error) {
LOG_ERR("ZIP", "Decompression failed"); LOG_ERR("ZIP", "Decompression failed");
break; break;
} }
// InflateStatus::Ok: output buffer full, continue // InflateStream::Status::Ok: output buffer full, continue
} }
free(outputBuffer); free(outputBuffer);
free(fileReadBuffer); free(fileReadBuffer);
return success; // ctx.reader destructor frees the ring buffer return success; // inflate destructor frees the decompressor state + window
} }
LOG_ERR("ZIP", "Unsupported compression method"); LOG_ERR("ZIP", "Unsupported compression method");
+16
View File
@@ -65,6 +65,18 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen); einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
} }
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
}
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) { void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) { if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1); einkDisplay.requestResync(1);
@@ -77,6 +89,10 @@ void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
uint8_t* HalDisplay::lendFrameBufferStorage(uint32_t* sizeOut) { return einkDisplay.lendBuildStorage(sizeOut); }
void HalDisplay::returnFrameBufferStorage() { einkDisplay.returnBuildStorage(); }
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
} }
+19
View File
@@ -39,6 +39,17 @@ class HalDisplay {
bool fromProgmem = false) const; bool fromProgmem = false) const;
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
// while the panel refreshes on its own. The framebuffer must stay untouched
// until waitRefreshComplete(), and the caller must rebuild the differential
// baseline before the next differential update (the tiled grayscale cleanup
// does). Panels without deferral fall back to a blocking refresh.
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
// Block until a pending deferred refresh completes (no-op when none is).
void waitRefreshComplete();
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
// false where it falls back to a blocking refresh.
bool supportsAsyncRefresh() const;
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Power management // Power management
@@ -47,6 +58,14 @@ class HalDisplay {
// Access to frame buffer // Access to frame buffer
uint8_t* getFrameBuffer() const; uint8_t* getFrameBuffer() const;
// Lend the framebuffer's ~48 KB STORAGE to a memory-hungry phase (chapter
// builds) without freeing it: the allocation never moves, so repeated loans
// cannot fragment the heap (free+realloc measurably did). No display calls
// between lend and return; the panel keeps its last refreshed image. The
// buffer comes back white — redraw fully. Returns nullptr if already lent.
uint8_t* lendFrameBufferStorage(uint32_t* sizeOut);
void returnFrameBufferStorage();
// X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed // X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed
// to the gray region in physical panel coordinates (no-arg = full frame). // to the gray region in physical panel coordinates (no-arg = full frame).
// Call after the BW base frame is displayed and before the grayscale planes // Call after the BW base frame is displayed and before the grayscale planes
+9
View File
@@ -0,0 +1,9 @@
{
"name": "miniz",
"version": "11.3.2",
"description": "Vendored miniz (tinfl inflate only) + InflateStream wrapper",
"build": {
"srcDir": "src",
"includeDir": "src"
}
}
+165
View File
@@ -0,0 +1,165 @@
#include "InflateStream.h"
#include <BuildScratch.h>
#include <cstdlib>
#include <cstring>
#include "MinizConfig.h"
namespace {
// tinfl's window must be a power of two; TINFL_LZ_DICT_SIZE is 32768.
constexpr size_t WINDOW_SIZE = TINFL_LZ_DICT_SIZE;
// tinfl_decompressor holds mz_uint32 arrays; 8 keeps the window aligned too.
constexpr size_t STATE_ALIGNED = (sizeof(tinfl_decompressor) + 7) & ~size_t{7};
} // namespace
InflateStream::~InflateStream() { deinit(); }
bool InflateStream::init(const bool streaming) {
// Every consumer constructs a fresh stream per operation, so acquire storage
// from scratch each init (releasing any prior backing first).
deinit();
// During a framebuffer loan the lent 48KB is up for grabs: state (~11KB) +
// window (32KB) fit inside it, so a chapter-build inflate costs the heap
// nothing. Absent (or already claimed): plain heap, freed in deinit().
const size_t needed = STATE_ALIGNED + (streaming ? WINDOW_SIZE : 0);
arenaBase = buildscratch::claim(needed);
if (arenaBase) {
state = reinterpret_cast<tinfl_decompressor*>(arenaBase);
window = streaming ? arenaBase + STATE_ALIGNED : nullptr;
} else {
// Raw malloc (not makeUniqueNoThrow): the header keeps tinfl_decompressor
// an incomplete type so consumers never include miniz; both blocks are
// freed in deinit()/the destructor.
state = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
if (!state) return false;
if (streaming) {
window = static_cast<uint8_t*>(malloc(WINDOW_SIZE));
if (!window) return false; // state kept; deinit()/next init reclaims it
}
}
tinfl_init(state);
windowPos = 0;
pendingStart = 0;
pendingLen = 0;
inPtr = nullptr;
inAvail = 0;
fill = nullptr;
fillCtx = nullptr;
inputExhausted = false;
zlibWrapped = false;
finished = false;
oneShotStart = nullptr;
return true;
}
void InflateStream::deinit() {
if (arenaBase) {
buildscratch::release(arenaBase);
arenaBase = nullptr;
} else {
free(state);
free(window);
}
state = nullptr;
window = nullptr;
}
void InflateStream::setSource(const uint8_t* src, const size_t len) {
inPtr = src;
inAvail = len;
inputExhausted = true; // the whole input is present; nothing more will come
}
void InflateStream::setFill(const FillFn fn, void* ctx) {
fill = fn;
fillCtx = ctx;
}
InflateStream::Status InflateStream::readAtMost(uint8_t* dest, const size_t maxLen, size_t* produced) {
*produced = 0;
if (!state) return Status::Error;
const bool streaming = window != nullptr;
if (!streaming && !oneShotStart) oneShotStart = dest;
for (;;) {
// Drain window bytes left over from a previous tinfl call. In ring mode
// tinfl may produce more than the caller asked for in one shot -- the
// overshoot stays pending in the window until a later readAtMost.
if (pendingLen > 0) {
size_t n = maxLen - *produced;
if (n > pendingLen) n = pendingLen;
memcpy(dest + *produced, window + pendingStart, n);
pendingStart += n;
pendingLen -= n;
*produced += n;
}
if (*produced == maxLen) {
return (finished && pendingLen == 0) ? Status::Done : Status::Ok;
}
if (finished) return Status::Done;
if (inAvail == 0 && !inputExhausted && fill) {
inAvail = fill(fillCtx, &inPtr);
if (inAvail == 0) inputExhausted = true;
}
const mz_uint32 flags = (zlibWrapped ? TINFL_FLAG_PARSE_ZLIB_HEADER : 0) |
(inputExhausted ? 0 : TINFL_FLAG_HAS_MORE_INPUT) |
(streaming ? 0 : TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
size_t inBytes = inAvail;
tinfl_status status;
size_t outBytes;
if (streaming) {
// Ring mode invariant: tinfl derives its wrap mask from
// (cursor offset + avail_out), so avail_out MUST always reach the end of
// the 32KB window -- never cap it to the caller's remaining space.
outBytes = WINDOW_SIZE - windowPos;
status = tinfl_decompress(state, inPtr, &inBytes, window, window + windowPos, &outBytes, flags);
pendingStart = windowPos;
pendingLen = outBytes;
windowPos += outBytes;
if (windowPos == WINDOW_SIZE) windowPos = 0;
} else {
// One-shot: back-references resolve directly inside the destination buffer.
outBytes = maxLen - *produced;
status = tinfl_decompress(state, inPtr, &inBytes, oneShotStart, dest + *produced, &outBytes, flags);
*produced += outBytes;
}
inPtr += inBytes;
inAvail -= inBytes;
if (status == TINFL_STATUS_DONE) {
finished = true; // drain any pending window bytes on the next pass
continue;
}
if (status < TINFL_STATUS_DONE) return Status::Error; // corrupt stream / adler mismatch
// TINFL_STATUS_NEEDS_MORE_INPUT loops back to the fill above; once the fill
// runs dry the HAS_MORE_INPUT flag drops and tinfl either finishes or fails
// (truncated stream) instead of spinning.
if (status == TINFL_STATUS_NEEDS_MORE_INPUT && inputExhausted && inAvail == 0) {
return Status::Error;
}
if (*produced == maxLen) {
return (finished && pendingLen == 0) ? Status::Done : Status::Ok;
}
}
}
bool InflateStream::read(uint8_t* dest, const size_t len) {
size_t total = 0;
while (total < len) {
size_t produced = 0;
const Status status = readAtMost(dest + total, len - total, &produced);
total += produced;
if (status == Status::Error) return false;
if (status == Status::Done) return total == len;
if (produced == 0) return false; // no progress safeguard
}
return true;
}
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include <cstddef>
#include <cstdint>
// Forward declaration keeps miniz out of consumer translation units; the
// decompressor state is heap-allocated in the .cpp where the type is complete.
struct tinfl_decompressor_tag;
// Streaming deflate decompressor wrapping miniz's tinfl.
//
// Replaces the uzlib-backed InflateReader on the throughput paths (EPUB zip
// entries, PNG IDAT). tinfl decodes via lookup tables where uzlib walks the
// Huffman tree bit-by-bit -- several times faster on this CPU -- at the cost
// of a larger decompressor state (~11KB, transient for the scope of the
// stream; taken from the lent framebuffer bytes via buildscratch::claim()
// when a FrameBufferLoan is active, heap otherwise). FontDecompressor
// intentionally stays on InflateReader:
// its one-shot flash-resident group decompressions are tiny, and the render
// path should not carry the extra state allocation.
//
// Two modes:
// init(false) -- one-shot: the destination buffer holds the ENTIRE output,
// so back-references resolve inside it and no 32KB window is
// allocated. read()/readAtMost() must be driven with
// contiguous, forward-only slices of that one buffer
// (a single read(dest, totalSize) is the common case).
// init(true) -- streaming: allocates a 32KB window; output can go to any
// buffer in any-sized chunks across calls.
//
// Input is either a single contiguous buffer (setSource) or pulled on demand
// through a fill callback (setFill): return the number of bytes available and
// point *data at them (valid until the next fill call); return 0 at end of
// input. Call setZlibWrapped() before the first read when the stream has a
// zlib header (e.g. PNG IDAT).
class InflateStream {
public:
enum class Status {
Ok, // Output buffer full; more decompressed data remains.
Done, // Stream ended cleanly. produced may be < maxLen.
Error, // Corrupt/truncated stream, or decompression failed.
};
using FillFn = size_t (*)(void* ctx, const uint8_t** data);
InflateStream() = default;
~InflateStream();
InflateStream(const InflateStream&) = delete;
InflateStream& operator=(const InflateStream&) = delete;
// Allocate decompressor state (and the 32KB window when streaming) and reset
// stream state. Reuses existing allocations on repeated calls. Returns false
// on OOM.
bool init(bool streaming);
// Free the decompressor state and window.
void deinit();
// Provide the entire compressed input as one contiguous buffer.
void setSource(const uint8_t* src, size_t len);
// Provide compressed input on demand. ctx is passed back to fn verbatim.
void setFill(FillFn fn, void* ctx);
// Declare the input zlib-wrapped (2-byte header + trailing adler32).
void setZlibWrapped() { zlibWrapped = true; }
// Decompress exactly len bytes into dest. Returns false if the stream ends
// or errors before producing len bytes.
bool read(uint8_t* dest, size_t len);
// Decompress up to maxLen bytes into dest; *produced gets the byte count.
Status readAtMost(uint8_t* dest, size_t maxLen, size_t* produced);
private:
tinfl_decompressor_tag* state = nullptr; // ~11KB: heap, or inside the claimed build scratch
uint8_t* window = nullptr; // 32KB ring, streaming mode only
uint8_t* arenaBase = nullptr; // non-null when state/window live in lent framebuffer bytes
size_t windowPos = 0; // ring write cursor
// Decompressed-but-undelivered region of the window (tinfl can overshoot the
// caller's requested length; the overshoot waits here for the next read).
size_t pendingStart = 0;
size_t pendingLen = 0;
const uint8_t* inPtr = nullptr;
size_t inAvail = 0;
FillFn fill = nullptr;
void* fillCtx = nullptr;
bool inputExhausted = false;
bool zlibWrapped = false;
bool finished = false;
// One-shot mode: tinfl needs the output buffer start for back-references.
uint8_t* oneShotStart = nullptr;
};
+35
View File
@@ -0,0 +1,35 @@
/* CrossPoint only needs miniz's low-level streaming inflate (tinfl). The
* archive, deflate, stdio, and zlib-compatibility layers are compiled out so
* the vendored library stays small and never touches the filesystem or clock.
* Include this header instead of <miniz.h> so every translation unit sees the
* same configuration. */
#pragma once
#define MINIZ_NO_STDIO
#define MINIZ_NO_TIME
#define MINIZ_NO_ARCHIVE_APIS
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#define MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
// The ESP32 mask ROM exports tinfl_* at fixed addresses via DIRECT linker
// script assignments (e.g. "tinfl_decompress = 0x...;" in the ROM .ld),
// which override object-file definitions -- without these renames the
// firmware silently binds to the ROM's 2021 build (TINFL_LESS_MEMORY, a
// different tinfl_decompressor layout) and corrupts inflate state on real
// data. Rename so the linker can never capture them. The prefix is
// crosspoint_ (NOT freeink_) so a future branch that links FreeInkBook's
// identically-renamed copy does not collide.
#define tinfl_decompress crosspoint_tinfl_decompress
#define tinfl_decompress_mem_to_heap crosspoint_tinfl_decompress_mem_to_heap
#define tinfl_decompress_mem_to_mem crosspoint_tinfl_decompress_mem_to_mem
#define tinfl_decompress_mem_to_callback crosspoint_tinfl_decompress_mem_to_callback
#define mz_crc32 crosspoint_mz_crc32
#define mz_adler32 crosspoint_mz_adler32
#define mz_free crosspoint_mz_free
// Include the vendored miniz by relative path: ESP-IDF ships a ROM miniz.h
// with the SAME include guard but a different (TINFL_LESS_MEMORY) struct
// layout -- resolving <miniz.h> through the platform include path would
// silently compile against the wrong structures.
#include "../third_party/miniz.h"

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