Compare commits

...
Author SHA1 Message Date
Justin Mitchell 4e12083566 Add anti-aliasing mode translations (Full/Fast)
Add STR_TEXT_AA_FULL and STR_TEXT_AA_FAST translation strings across all supported languages. Also includes a complete retranslation of the Slovak language file.
2026-07-06 19:04:00 -04:00
Justin Mitchell cf4f7be8af Reformat anti-aliasing condition for readability
Consolidate multi-line conditional statement in GfxRenderer onto fewer lines without changing logic
2026-07-06 19:01:21 -04:00
Justin Mitchell e69241ef54 Reverts UI font change and adds setting for AA sd fonts 2026-07-06 18:59:25 -04:00
Justin Mitchell 72b99a2a9c Enable compression for ui fonts
Add --compress flag to font generation command and regenerate the font bitmap data with compression enabled. This reduces the bitmap array size while maintaining the same 2-bit mode and character intervals.
2026-07-05 19:46:43 -04:00
Justin Mitchell 62448b3c08 Add anti-aliased font rendering with dithering
Replace solid black rendering of anti-aliased font edges with period-2 dithering patterns. Full coverage pixels render as solid ink, while partial coverage pixels use checkerboard patterns (50% for dark grey, 25% for light grey) matching fillRectDither behavior. This prevents edge pixels from bolding to black on BW displays and produces proper anti-aliasing for 2-bit fonts.
2026-07-05 16:06:56 -04:00
Tom-Inge Larsen 3e1dd31e53 feat(reader): End of Book next-book suggestions (#2499) (#2532) 2026-07-04 23:12:59 +03:00
685d4e88f9 feat: Lazy incremental EPUB section indexing (#2452)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-07-04 21:21:24 +03:00
Jakub Sypiański 79b4d63ee2 fix: settings persist on font clear, reserve() before push_back, minor (#2519) 2026-07-03 18:58:24 +03:00
Leopoldo Pla SempereandJiangoJ 57447a56a6 feat(epub): improve text-decoration support (#2397)
Co-authored-by: JiangoJ <jiangj2620@gmail.com>
2026-07-02 12:35:24 +03:00
Rahat Ahmed 5c86bfe1fd feat(network): add device serial number to web UI (#2506) 2026-07-02 00:53:55 +03:00
Justin Mitchell 4cdefb137c Update README with firmware flashing warnings and crosspet status
Removed warning about flashing Papyrix on USB-locked devices and updated crosspet status to unmaintained.
2026-07-01 14:22:27 -04:00
Justin Mitchell e7f3406704 fix: Add socket module import to build-sd-fonts script (#2504) (#2505) 2026-07-01 12:10:15 -04:00
Bastian 6f08a88b12 fix: changing translation in czech to fix overflowing navigation (#2502) 2026-07-01 11:58:24 -04:00
Tom-Inge LarsenandClaude Opus 4.8 e4d3981469 fix(reader): correct slider side-button direction and legend on X3 (#2402) (#2428)
## Summary

* **What is the goal of this PR?** Fixes #2402. On the X3, the "Time to
Sleep"
picker's 5-minute side buttons were inverted (left increased, right
decreased)
  and the on-screen legend didn't match the physical buttons.
* **What changes are included?**
  * Flip the large-step (±5 min) direction on X3 so the left side button
decreases and the right increases, matching the layout. X4 is unchanged.
* Add a device-specific step-hint string
(`STR_SLEEP_TIMER_STEP_HINT_X3`)
selected via `gpio.deviceIsX3()`. X3 shows `+/-: 1 min Side buttons: 5
min`;
    X4 keeps the original `Left/Right: 1 min  Up/Down: 5 min`.
* Same fix applied to both slider pickers that read the raw up/down side
    buttons: the Time-to-Sleep picker and the "Go to %" picker
(`EpubReaderPercentSelectionActivity`), each with its own X3 hint string
    (`STR_SLEEP_TIMER_STEP_HINT_X3`, `STR_PERCENT_STEP_HINT_X3`).

## Additional Context

* **Root cause:** the X3's side buttons sit one on each edge of the
screen
(power on top), whereas the X4 has a vertical up/down rocker on the
right
edge. So `BTN_UP` is physically the *left* button on X3 but the *top*
button
  on X4. The picker mapped up→+5 / down→−5 unconditionally, which reads
naturally on the X4 rocker but inverted on the X3's left/right buttons.
The
  static legend ("Up/Down: 5 min") had the same X3-only mismatch.
* The codebase already detects the device at runtime and handles this
layout
difference elsewhere (e.g. `LyraTheme::drawSideButtonHints` draws "Up on
left,
Down on right" for X3), so this reuses the same `gpio.deviceIsX3()`
signal.
* Swept the codebase for other side-button sliders:
`ClockOffsetActivity` already
uses the `Next/Previous` (`ButtonNavigator`) abstraction, which resolves
to
left=decrement / right=increment on X3, so it needs no change. List/page
navigation (Wi-Fi, KOReader sync, BMP viewer) and the keyboard cursor
are also
unaffected. The two slider pickers above were the only ones using raw
up/down.
* **X4 is untouched** — same direction, same legend wording as before.
* **Tested on X3 hardware:** left side button now decreases, right
increases, and
  the legend matches. X4 not yet tested on device (no unit on hand); its
  behaviour and strings are unchanged from master.
* **Translations:** the new `STR_SLEEP_TIMER_STEP_HINT_X3` and
`STR_PERCENT_STEP_HINT_X3` were added to all 26 languages, but the
non-English
  ones are AI-generated and would benefit from a native-speaker pass —
particularly Hebrew (RTL ordering with a leading `+/-`), Kazakh, and
Vietnamese.

---

### AI Usage

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:06:31 -04:00
Bastian c9d27826f9 docs: update TOC in userguide (#2479) 2026-06-30 08:48:21 +03:00
Bastian 13bd00bf9d fix: sort sleep screen menu options to be more logically consistent (#2480) 2026-06-30 08:47:25 +03:00
Uri Tauberandpablohc fef39da23c feat: Selection Popup (#2358)
Co-authored-by: pablohc <pablonoviello@outlook.com>
2026-06-29 16:59:25 -04:00
Uri Tauber fd43ca2fe1 chore: Initial multi-core compatibility (#2294) 2026-06-29 21:57:21 +03:00
Pietro Campagnano 28255061fb fix(icons): align home menu icons with their labels (#2470) 2026-06-29 13:59:54 -04:00
Julia Nguyen 43ff1b6644 Merge remote-tracking branch 'origin/master' into develop 2026-06-29 12:29:58 -04:00
Егор Мартынов 2754a5ff01 feat: add Vollkorn font (#2473)
CI (build) / clang-format (push) Canceled after 0s
CI (build) / cppcheck (push) Canceled after 0s
CI (build) / build (push) Canceled after 0s
CI (build) / unit-tests (push) Canceled after 0s
CI (build) / Test Status (push) Canceled after 0s
2026-06-29 15:54:01 +03:00
Justin Mitchell 6edeed73c5 fix: x4 sleep/boot ghosting (#2471) 2026-06-29 12:04:33 +03:00
Justin Mitchell 8ac302018c fix: x4 ghosting by bumping sdk (#2469) 2026-06-28 22:38:35 +03:00
Rodrigo Matsuura a11517a62d fix: update Portuguese (Brasil) translations (#2458) 2026-06-28 15:07:52 +03:00
Bastian cbaa498ccc docs: adding quick resume option and quick resume on timeout to userguide (#2425) 2026-06-27 22:40:21 +03:00
Justin Mitchell ebebc6f202 chore: migrate from open-x4-sdk to freeink-sdk (#2449)
This PR moves us from the xteink openx4 SDK to the freeink sdk from
https://freeink.org. Out of the box there are NO changes needed in the
firmware to support this swap, it all magically works as is. However as
we support more than just the x3/x4 devices, this sdk allows us to pass
env vars into the build commands to include support for other devices.
As support for new hardware such as touch screens and bluetooth are
added the xteink builds decide at compile time if the libraries are used
or not. For example right now the freeinkui and icons libraries are in
the platform.io file but as they are not used anywhere, they won't be
included in the final build. Once the touch branch and sd themes branch
are merged in this sdk is required for them to function correctly. All
the docs for freeink are available at freeink.org/docs. x4/x3 is a
single binary build unlike other devices that will build unique binaries
for each device. Eventually we will want to remove a lot of the manual
isx3 type stuff from our firmware and go through the boardsupport api
the sdk provides as it will generalize everything into one common system
that any device can support. The upcoming touch branch does a lot of
this for us but this initial PR is JUST to get the sdk swapped over
without any code changes to show seamless integration without any
regressions.
2026-06-27 21:22:58 +03:00
Julia 970b2c6ca1 chore: release 1.4.1 (#2447)
Compile Release / build-release (push) Canceled after 0s
## Improvements

* Moved the File Manager breadcrumb into the Contents card header for a
cleaner, more consistent interface.
* Updated the Wireless Transfer section of the User Guide with clearer
instructions.
* Battery status bar indicator no longer changes position when adding or
removing bookmarks.

## Performance

* Optimized path normalization for faster file handling.
* Significantly improved bookmark rendering by removing unnecessary
XPath lookups.
* Optimized dithered rectangle drawing (fillRectDither) using a
byte-aligned rendering implementation, improving display performance on
supported devices.

## Bug Fixes

* Fixed an issue where the Inverted Orientation label was incorrectly
combined with the Color Filter label for Geman localization.
* Fixed excessive ghosting on the X3 cover screen during sleep
2026-06-26 17:40:04 -04:00
Julia Nguyen b6ce599b20 fix: address release review feedback 2026-06-26 16:22:20 -04:00
Juliaandcoderabbitai[bot] f54eab2725 fix: typo in STR_ORIENTATION_INVERTED Spanish translation
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-26 16:13:19 -04:00
Justin Mitchell 0d4c9ab91b Bump version to 1.4.1 2026-06-26 15:48:19 -04:00
Julia 0daa9db243 fix: keep status bar indicators stable when toggling bookmarks (#2444) 2026-06-26 19:26:33 +03:00
Uri TauberandRyan Mercado a2f2eea79e perf: Optimize fillRectDither with Byte-Aligned fillRectImpl (#2270)
## Summary

* **What is the goal of this PR?**
Replace the pixel-by-pixel `fillRectDither` implementation with a new
byte-aligned `fillRectImpl` that eliminates per-pixel
`rotateCoordinates` calls and Read-Modify-Write bitwise loops, yielding
a significant rendering speedup on ESP32 E-ink framebuffers.

Ported from @rhythmerc's crosspoint-reader fork commit 27ea625.

* **What changes are included?**

- **`GfxRenderer.cpp` — `fillRectDither` refactor:** The existing
`if/else if` chain is replaced with a `switch` statement that delegates
each `Color` case to the new `fillRectImpl<Color>()` template,
eliminating runtime branching.

- **`GfxRenderer.cpp` — new `fillRectImpl<C>()` template:** Core of the
optimization. Key behaviors:
    - Clips the rectangle in logical space upfront.
- Rotates only **2 opposing corner points** (top-left and bottom-right)
into physical framebuffer space instead of rotating every pixel
individually.
- Derives physical-space `byteStart`/`byteEnd` and precomputes
`headMask` / `tailMask` for MSB-first partial-byte boundaries,
performing RMW only on the edge bytes.
- **Solid fills (`Black` / `White`):** Uses `memset` for all interior
full-byte runs per row — no per-pixel writes.
- **Dithered fills (`LightGray` / `DarkGray`):** Precomputes both parity
variants of `blackMask` (even/odd `py`) **outside** the row loop,
eliminating the previously re-evaluated 8-bit construction loop on every
physical row. Interior full bytes are then written with a single
`memset(whiteMask)`.
- Uses `if constexpr` throughout to dispatch on `Color` at compile time,
generating zero runtime branches per template instantiation.

- **`GfxRenderer.h`:** Declares the new private `fillRectImpl<Color>()`
template method with an explanatory doc-comment.

- **Explicit template instantiations** added for all four active `Color`
variants (`Black`, `White`, `LightGray`, `DarkGray`).

## Additional Context

* **Performance:** The primary motivation is ESP32 E-ink framebuffer
performance. The old path called `rotateCoordinates` and did a full RMW
for every single pixel in the rectangle. The new path calls
`rotateCoordinates` exactly **twice** per fill regardless of rectangle
size, then operates at byte granularity — a complexity reduction from
O(W×H) coordinate transforms to O(1).
* **Dither correctness:** The `blackMask` precomputation relies on the
dither pattern having period 2 in both logical X and Y, which makes the
per-row byte pattern repeat with period 2 in `py`. Reviewers should
verify the `lxBase`/`lyBase` derivations for all four orientations
(`Portrait`, `PortraitInverted`, `LandscapeClockwise`,
`LandscapeCounterClockwise`) match the inverse of `rotateCoordinates`.
* **Edge case — single-byte rows:** When `byteStart == byteEnd`, the
head and tail masks are ANDed together into a single `rectMask` to avoid
double-masking the same byte. This path should be tested with narrow
rectangles (width < 8px).
* **No behavioral change for `Color::Clear`:** The `Clear` case exits
early via `if constexpr` and is a no-op, matching the original behavior.

---

### AI Usage

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

Did you use AI tools to help write this code? _**< YES >**_

---------

Co-authored-by: Ryan Mercado <rmercado@firstdollar.com>
2026-06-26 12:16:25 -04:00
Ankit 86a9b9c4a2 docs: update user guide wireless transfer section (#2369)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Closes #1407
2026-06-26 08:25:52 +03:00
Julia Nguyen ba44978ac4 Merge branch 'master' into develop 2026-06-25 17:51:56 -04:00
Pietro Campagnano 555f76da88 feat: move file manager breadcrumb into contents card header (#2430) 2026-06-26 00:45:43 +03:00
Justin Mitchell 0a57c0a5a7 Fix X3 display ghosting on cover screen transitions
Force display resync on X3 when HALF refresh is requested to clear prior content before rendering. Add grayscale preconditioning for X3's UC81xx controller to even out single-pixel dithering artifacts that appear as speckle with its turbo BW waveform.
2026-06-25 17:42:19 -04:00
Uri Tauber a09aef0889 fix: Optimize Bookmark Rendering by Removing XPath Lookup (#2417)
## Summary

fix #2414 

### Root Cause

`updateBookmarkFlag()` was introduced in commit 1db1442 and is executed
on every render (every page turn). The function calls:

`ProgressMapper::toSavedProgress()`
→ `ChapterXPathResolver::findXPathForProgress()`

This path decompresses the current EPUB section content twice:

1. To count visible characters.
2. To resolve the corresponding XPath.

For larger sections (e.g. ~133 KB decompressed content), this adds
approximately **1 second of I/O overhead per page turn**, with the cost
increasing as chapter size grows.

### Fix

`updateBookmarkFlag()` only needs to determine whether a bookmark falls
within the currently displayed page range.

The required information is already available during rendering:

* `currentPage`
* `section->pageCount`
* `currentSpineIndex`

Instead of converting the current location to a saved progress object
(and resolving an XPath), the implementation now computes the current
page's progress range directly and compares bookmark percentages against
that range.

This is effectively the same percentage-based matching logic already
used as a fallback in `bookmarkMatchesProgress()` when XPath matching is
unavailable.

---

### AI Usage

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

Did you use AI tools to help write this code? _**< PARTIALLY >**_
2026-06-25 09:20:24 -04:00
Bastian 8626d69f46 fix: small translation changes for german (#2420) 2026-06-25 09:29:05 +03:00
Uri Tauber fc89e57e69 perf: optimise normalisePath (#2162) 2026-06-25 09:01:50 +03:00
Julia 487613b082 fix: split inverted orientation label from color filter label (#2421)
## Summary

* **What is the goal of this PR?** 
* Fix the “Inverted” translation so reader orientation and color/filter
inversion can use separate labels.
* **What changes are included?**
* Adds `STR_ORIENTATION_INVERTED` for the inverted portrait orientation
option.
* Updates the reader orientation setting to use
`STR_ORIENTATION_INVERTED` instead of reusing `STR_INVERTED`.
* Leaves `STR_INVERTED` for the sleep cover filter and tilt page-turn
mode
  * Adds the new orientation string across all 26 locale YAML files.

## Additional Context

* The original issue was found by a user in German, where `STR_INVERTED`
was translated as `Hochformat 180°`, which made sense for orientation
but not for color filters.
* This is a UI-label-only change. It does not change persisted
orientation values or settings behavior.
* Reviewer note: non-English wording may still benefit from
native-speaker review, especially for the new orientation-specific
labels and to verify the interchangeable usage between inverted color
and inverted tilt page turn direction.
---

### AI Usage

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

Did you use AI tools to help write this code? _**< YES >**_
2026-06-25 00:47:06 -04:00
JuliaandHusam Younis 8d5b119644 fix: sync master into develop (#2423)
Sync develop branch with master

Co-authored-by: Husam Younis <youhusam@gmail.com>
2026-06-24 22:07:13 -04:00
Husam Younis 7271c00d35 feat: Allow statusbar clock to be on the left (#2359) 2026-06-25 00:23:23 +03:00
Uri Tauber 6e8dbd7f23 chore: Update version to 1.4.0 (#2283)
Compile Release / build-release (push) Canceled after 0s
2026-06-24 18:30:32 +03:00
Uri Tauber c4b1d9644a fix: correct font size selection (#2410) 2026-06-24 17:01:10 +03:00
Justin Mitchell f6e59aab72 feat: Add displayGrayscaleBase method for differential refresh
(#2334)

Introduces a new display method that prepares the framebuffer as a base
frame for grayscale overlays. On X3 panels, this uses the OEM
differential base waveform (AA-pre-BW) without forcing a resync. Other
panels fall back to normal display with configurable refresh mode.


Dependent upon matching SDK commit to work
2026-06-24 07:53:19 -04:00
Uri Tauber 9ad2da0950 fix: add seven missing hebrew translations (#2409) 2026-06-24 09:34:40 +03:00
Uri TauberandJulia Nguyen 1db1442319 fix: several bookmarks UX improvments (#2372)
## Summary

This PR enhances the EPUB reader's bookmark system with two
complementary improvements: a per-page bookmark indicator icon and
toggle behavior on the existing long-press action.

---

### What Changed

**Bookmark Toggle (was: add-only)**

The long-press Confirm action now toggles bookmarks rather than always
adding. `addBookmark()` checks whether a bookmark with the same xpath
already exists in the in-memory cache:
- If found → removes it and shows "Bookmark removed."
- If not found → adds it and shows "Bookmark added."

A new `STR_BOOKMARK_REMOVED` translation string was added to support the
removal message.

**Bookmark Icon Indicator**

A `BookmarkIcon` is now drawn at the top-right corner of the page
whenever the current page has a bookmark. `updateBookmarkFlag()` is
called at render time to determine whether the current page is
bookmarked.

**In-Memory Bookmark Cache**

Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather
than being re-read from disk on every toggle. All subsequent add/remove
operations work against this cache and flush to disk, avoiding redundant
file reads on each bookmark action.

**Faster bookmarks list**

Previously, calculating "page X/Y" for each entry required decompressing
the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page
count, and page progress) in the bookmark JSON when saving, and restore
them when loading. This avoids the expensive `toCrossPoint()` loop in
`onEnter()`, significantly reducing the cost of initializing the
bookmarks list.

---

### Files Changed

- `EpubReaderActivity.cpp` — `addBookmark()` toggle logic,
`updateBookmarkFlag()` (new), icon rendering in `renderContents()`,
cache initialization in `onEnter()`
- `EpubReaderActivity.h` — new fields: `currentPageBookmarked`,
`bookmarkRemoved`, `cachedBookmarks`; new method declaration
`updateBookmarkFlag()`

---

### AI Usage

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

Did you use AI tools to help write this code? _**< PARTIALLY >**_

---------

Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-06-23 14:55:01 -04:00
Leopoldo Pla Sempere 362dcb2a65 feat: Smooth progressive JPEG cover upscales in BMP conversion (#2214) 2026-06-22 13:55:12 -04:00
Julia d1abcc00a2 feat: render grayscale epub images without text aa (#2393) 2026-06-22 17:15:27 +03:00
Zeph 0ce874f8d4 feat: footnote returns to original position on deep sleep to avoid losing reading position (#2394) 2026-06-22 15:28:42 +03:00
kygia 282514f755 fix(epub): flush displaced anchor before overwrite (#2336) (#2382)
## Summary

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

Fixes footnote links landing in start section instead of the specific
foot note target.

* **What changes are included?**

ChapterHtmlSlimParser.cpp: call flushPendingAnchor() before overwriting
pendingAnchorId. First id got lost due to consecutive non-block elements
carry ids, the first id was lost and the reader had no page to jump to,
so it defaulted to page 0.

## Additional Context

Tested with the epubs attached to to #2336. Need to clear .crosspoint/
cache after flashing so the anchor map gets rebuilt with the fix.

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

---

### AI Usage

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

Did you use AI tools to help write this code? 

Partially assisted with writing documentation.
2026-06-21 16:43:39 -04:00
SurprisedDuck d1e4650e19 fix: don't justify-stretch a leading no-break space (#2185) (#2298) 2026-06-21 16:12:14 +03:00
Justin Mitchell 370f87ea01 feat: add Spacing Modifier Letters range to font presets (#2194) 2026-06-21 09:14:42 +03:00
Leopoldo Pla Sempere 8b9a73a735 chore: update Spanish, Catalan, and Valencian translations (#2384) 2026-06-21 00:26:30 +03:00
Julia 29c69b3d8f fix: automatically connect to wifi for clock sync (#2379)
## Summary

* **What is the goal of this PR?** 
* Make manual clock sync work even when the device is not already
connected to Wi-Fi, so the user can start the sync flow directly from
settings instead of being blocked by connection state.
* **What changes are included?**
* `ClockSyncActivity` now launches the normal Wi-Fi selection flow
before syncing when the device is offline.
* After Wi-Fi selection succeeds, clock sync resumes automatically and
performs the existing forced NTP sync.
* If clock sync had to bring Wi-Fi up, the activity disconnects and uses
the existing silent restart cleanup path to avoid leaving the device in
a fragmented post-Wi-Fi heap state.
* The completion UI now only advertises Back, matching the updated input
handling.

## Additional Context

* Existing behavior is unchanged when Wi-Fi is already connected: the
activity syncs immediately.
* Cancelling Wi-Fi selection exits the clock sync flow.

---

### AI Usage

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

Did you use AI tools to help write this code? _**< YES >**_
2026-06-20 22:09:28 +03:00
Matteo Scopel 4b5a84dd81 chore: update the Italian translation (#2376) 2026-06-20 22:08:35 +03:00
muhasandmuhas b9874c8114 fix(lang): [russian] new string (#2374)
Co-authored-by: muhas <mail@muhas.name>
2026-06-19 13:15:15 +03:00
Paul Delestrac b1131795d8 feat: add live font preview pane to font selection screen (#2349)
## Summary
* Adds a live font preview pane to the font selection screen so users
can see how a font looks before committing to it.
* Changes
* A preview pane occupying the top 30% of the font selection screen,
rendering sample pangram text in the previewed font
* A two-step confirm flow: first press (enter button) previews the font,
second press selects it
* Back restores the original font settings, so browsing has no side
effects
* Layout dimensions cached in `onEnter()` to avoid redundant
recalculation between `loop()` and `render()`

## Additional Context
* Preview sample text is hardcoded English; didn't want to use AI for
translation as I would not be able to verify the output in most
languages...
* The preview pane reduces visible list height; this is compensated by
passing the reserved height into `getNumberOfItemsPerPage`

---
### AI Usage
Did you use AI tools to help write this code? **PARTIALLY**
AI use for assisting in coding and in writing the PR description.
2026-06-18 18:16:28 -04:00
Pedro CardosoandClaude Opus 4.8 5e990b3991 fix: prevent progress.bin corruption from interrupted writes (#2275) (#2305)
## Summary

* **What is the goal of this PR?**
Fixes #2275. A book could get stuck reopening on an old page, with
progress no
longer saving and neither "Delete Book Cache" nor "Clear Reading Cache"
able to
fix it. Root cause: `progress.bin` was written truncate-in-place, so an
interrupted write (power loss, or a crash mid-SPI during sleep) left it
with a
broken FAT cluster chain that the firmware could neither rewrite nor
delete —
  recovery required `fsck`/manual deletion on a host PC.

  Confirmed in the SDK: `SDCardManager::openFileForWrite` opens with
`O_RDWR | O_CREAT | O_TRUNC`, so the canonical file is zeroed before the
few
progress bytes are rewritten — exactly the window that corrupts the FAT
chain.

* **What changes are included?**
  * New shared helper `ProgressFile::writeAtomic()`
    (`src/activities/reader/ProgressFile.h`): writes progress to
    `progress.bin.tmp`, flushes and closes it, then `remove`s the old
`progress.bin` and `rename`s the temp into place. An interrupted write
now
only ever damages the throwaway temp; the canonical file is never torn.
  * All three readers route their progress saves through the helper:
EPUB (`EpubReaderUtils.h`), `TxtReaderActivity`, `XtcReaderActivity` —
they
    all shared the identical vulnerable pattern.
* Minor: `EpubReaderUtils::saveProgress` now takes `const Epub&` (clears
a
    cppcheck `constParameterReference` finding).

## Additional Context

* **Crash-safe, not metadata-atomic.** On FAT the replace is `remove` +
`rename`
(two directory ops; SdFat's `rename` won't overwrite, hence
remove-first). A
crash between them leaves *neither* file, which reads as "no saved
progress" on
next launch — a harmless reset to an old page, never a
corrupt/unclearable file.
  The guarantee is that `progress.bin` is never half-written.
* **Prevents, does not repair.** This stops new corruption on healthy
cards. It
cannot fix an already-corrupted `progress.bin` (removing it may itself
fail at
the FAT level) — those still need `fsck`/manual deletion, as in the
issue's
  workaround.
* **Known follow-up (out of scope here):** a crash *while writing the
temp* can
leave an orphan `progress.bin.tmp`. It's harmless and self-healing (the
next
save overwrites it, and it never blocks reading progress), but a
boot-time
  orphan-`.tmp` cleanup would be a tidy follow-up.
* **Focus areas for review:** the close-before-rename ordering in
  `ProgressFile.h` and the remove-before-rename rationale.

## Verification

* `./bin/clang-format-fix` — clean
* `pio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
* `pio run` — SUCCESS (RAM 30.9%, Flash 78.8%; footprint essentially
unchanged)
* Tested on a **Xteink X4** device: open book, turn pages, sleep/exit,
reopen —
  progress now restores to the navigated page across all three readers
  (EPUB / TXT / XTC).

---

### AI Usage

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:01:56 -04:00
Uri Tauber ff1951c715 fix: correct behaviour for prev/next side buttons (#2373)
## Summary

* **What is the goal of this PR?** fix #2365
2026-06-18 13:13:35 -04:00
Ankit 121c0690b0 fix: use STR_SELECT instead of STR_OPEN in bookmark button hint (#2371) 2026-06-18 19:58:47 +03:00
16b0853654 fix(epub): NFC-normalize EPUB text so NFD diacritics render correctly (#2277)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Co-authored-by: Julia <julia@uxj.io>
2026-06-18 13:00:01 +03:00
Uri Tauber 7d639cf880 fix: submodule pointer (#2368) 2026-06-17 22:35:12 +03:00
Justin MitchellandUri Tauber 22f3575064 feat: Support for Korean line breaks and glyph spacing (#2288)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-06-17 17:27:03 +03:00
darkbubluandClaude Opus 4.7 d4069aeae5 feat: long-press Confirm launches KOReader sync from EPUB (#1808)
## Summary
- Hold the menu (Confirm) button for ≥1s while reading an EPUB to launch
the existing `KOReaderSyncActivity` directly — replaces the three-step
path (open reader menu → scroll to Sync → confirm) with a single
gesture.
- Reuses `ReaderUtils::GO_HOME_MS` (same 1s threshold used by long-press
Back) and the existing `KOREADER_STORE.hasCredentials()` guard.
- Adds a Controls picker **"Long-press Menu"** (`longPressMenuFunction`,
default **Bookmark**) that **cycles through the available functions**
bound to the long-press gesture: `KOSync → Disabled → Bookmark`. The
field name and `LONG_PRESS_MENU_FUNCTION` enum are intentionally general
so future actions (dictionary lookup, table of contents, etc.) can be
appended without another schema migration. The setting is **not** a
binary toggle.
- Existing menu Sync entry still works — both call sites share one
extracted helper (`launchKOReaderSync`); no logic duplication.
- Short-press Confirm release is gated on duration so the reader menu
does not also open after a long press that *acts*, mirroring the
existing long-press Back pattern.
- **No-credentials fall-through:** `launchKOReaderSync()` now returns
whether it acted. When the function is set to KOSync but no KOReader
credentials are stored, the long-press is a no-op that **falls through
to open the reader menu** — so the menu stays reachable instead of the
hold silently swallowing the gesture. The release is only suppressed
when sync actually launched or surfaced a save error.

## Test plan
- [x] `pio run` succeeds clean for the `default` ESP32-C3 environment.
- [x] On-device, value **KOSync**, valid KOReader credentials:
long-press Confirm ≥1s → sync screen launches; get + update progress
return HTTP 200; release does **not** also open the reader menu; returns
to the same page.
- [x] On-device, value **KOSync**, **no** credentials: long-press
Confirm falls through and **opens the reader menu** (regression fix);
short-press also opens the menu.
- [x] On-device: menu → Sync still launches the same screen (shared
helper) and syncs (200/200).
- [x] On-device, value **Bookmark**: long-press drops a bookmark and
does **not** also open the menu.
- [x] On-device, value **Disabled**: long-press Confirm opens the menu
on release; no sync, no bookmark.
- [x] On-device: long-press Back still goes to the file browser
(unchanged path).
- [x] Heap: epub is released before the TLS handshake (frees ~16 KB);
min free heap stayed ~84 KB during sync, well above the safe floor. No
panics/OOM across the session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 17:10:26 +03:00
Mauvis LedfordandJulia 54d5a788a5 feat: add drag-and-drop to upload modal (#2290)
Co-authored-by: Julia <julia@uxj.io>
2026-06-17 16:53:31 +03:00
Ankit f66220a245 fix: enable Shift key for URL keyboard input (#2178) (#2357) 2026-06-17 16:32:36 +03:00
KymAndriyandKymAndriy 2266060e84 fix: Ukrainian translation (#2354)
Co-authored-by: KymAndriy <test@notamail.ua>
2026-06-16 14:02:23 +03:00
Julia 90039d7f4d fix(koreader): resolve element XPath progress against visible body text (#2308) 2026-06-15 22:03:32 -04:00
rafaelmsseandRafael Santos 16eb66d7cf fix: add missing HTML 4.01 named entities (#2352)
Co-authored-by: Rafael Santos <rmsantos@applaudostudios.com>
2026-06-15 22:25:37 +03:00
RoninandUri Tauber 02bab00be6 fix: swap reader menu navigation direction in CCW/inverted (#2321) (#2341)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
2026-06-15 22:19:34 +03:00
Mauvis Ledford 55a56914d7 fix(i18n): localize home empty-state strings (#2342) 2026-06-14 21:40:05 +03:00
143 changed files with 5546 additions and 1560 deletions
+4 -3
View File
@@ -1,3 +1,4 @@
[submodule "open-x4-sdk"] [submodule "freeink-sdk"]
path = open-x4-sdk path = freeink-sdk
url = https://github.com/crosspoint-reader/community-sdk.git url = https://github.com/Free-Ink/freeink-sdk.git
branch = main
+2 -2
View File
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized). * Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable. * Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification. * Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first. * Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage). * No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected. * Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file). * Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage) * lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables) * lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit) * src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager) * freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections * .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
### Hardware Abstraction Layer (HAL) ### Hardware Abstraction Layer (HAL)
+1 -5
View File
@@ -67,10 +67,6 @@ USB port or browser before assuming the device is locked. Only reach for the unl
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently > Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if > stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
> the firmware you flashed doesn't support OTA, **there is no way out**. > the firmware you flashed doesn't support OTA, **there is no way out**.
>
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
> Papyrix (or any other unsupported firmware) on a locked device.**
## Install firmware ## Install firmware
@@ -249,7 +245,7 @@ One of the best things about open source is that anyone can take the code in a d
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card. - [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games. - ~~[crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.~~ (Unmaintained)
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading. - [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
+67 -24
View File
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [CrossPoint User Guide](#crosspoint-user-guide) - [CrossPoint User Guide](#crosspoint-user-guide)
- [1. Hardware Overview](#1-hardware-overview) - [1. Hardware Overview](#1-hardware-overview)
- [Button Layout](#button-layout) - [Button Layout](#button-layout)
- [Taking a Screenshot](#taking-a-screenshot)
- [2. Power \& Startup](#2-power--startup) - [2. Power \& Startup](#2-power--startup)
- [Power On / Off](#power-on--off) - [Power On / Off](#power-on--off)
- [First Launch](#first-launch) - [First Launch](#first-launch)
@@ -14,7 +15,11 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.3 Browse Files Screen](#33-browse-files-screen) - [3.3 Browse Files Screen](#33-browse-files-screen)
- [3.4 Recent Books Screen](#34-recent-books-screen) - [3.4 Recent Books Screen](#34-recent-books-screen)
- [3.5 File Transfer Screen](#35-file-transfer-screen) - [3.5 File Transfer Screen](#35-file-transfer-screen)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers) - [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
- [Uploading Books](#uploading-books)
- [Removing a Book](#removing-a-book)
- [3.6 Settings](#36-settings) - [3.6 Settings](#36-settings)
- [3.6.1 Display](#361-display) - [3.6.1 Display](#361-display)
- [3.6.2 Reader](#362-reader) - [3.6.2 Reader](#362-reader)
@@ -23,21 +28,25 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) - [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds) - [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup) - [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
- [3.7 Sleep Screen](#37-sleep-screen) - [3.7 Sleep Screen](#37-sleep-screen)
- [Cover settings](#cover-settings)
- [Custom images](#custom-images)
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card) - [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
- [4. Reading Mode](#4-reading-mode) - [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning) - [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation) - [Chapter Navigation](#chapter-navigation)
- [Auto Page Turn](#auto-page-turn) - [Auto Page Turn](#auto-page-turn)
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only) - [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
- [Footnote Navigation](#footnote-navigation) - [Footnote Navigation](#footnote-navigation)
- [System Navigation](#system-navigation) - [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages) - [Supported Languages](#supported-languages)
- [5. Reader Menu](#5-reader-menu) - [5. Reader Menu](#5-reader-menu)
- [5.1 Chapter Selection](#51-chapter-selection) - [5.1 Chapter Selection](#51-chapter-selection)
- [5.2 Bookmarks](#52-bookmarks) - [5.2 Bookmarks](#52-bookmarks)
- [6. Current Limitations & Roadmap](#6-current-limitations--roadmap) - [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues & Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop) - [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
## 1. Hardware Overview ## 1. Hardware Overview
@@ -122,19 +131,43 @@ A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin. CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
1. Install the plugin in Calibre: #### Installing the Plugin in Calibre
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
- Download the zip file.
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
2. On the device: File Transfer -> Calibre Wireless, then join a network. If you don't already have the plugin installed:
3. Make sure your computer is on the same Wi-Fi network. 1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
2. Download the zip file.
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
4. Restart Calibre.
4. In Calibre, click "Send to device" to transfer books. #### Configuring the CrossPoint Plugin in Calibre
1. In Calibre select Preferences.
2. In the Preferences dialog select Plugins.
3. In Plugins search for "crosspoint".
4. Click on "Customize plugin".
5. Update the value for "Host" to match the IP for your device.
6. Leave the other settings as they are.
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
8. Restart Calibre.
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
#### Uploading Books
To upload a book using the CrossPoint plugin in Calibre:
1. On the device: File Transfer -> Calibre Wireless, then join a network.
2. Select one or more books.
3. Right-click on that selection.
4. Select "Send to Device" > "Send to main memory"
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
#### Removing a Book
Books cannot be removed from your device through Calibre. Use the web interface instead.
### 3.6 Settings ### 3.6 Settings
@@ -150,6 +183,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Cover" - The book cover image (Note: this is experimental and may not work as expected) - "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen - "None" - A blank screen
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise - "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected: - **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
@@ -162,6 +196,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion - "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion - "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
- **Status Bar**: Configure the status bar displayed while reading: - **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar - "None" - No status bar
@@ -241,6 +277,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter - "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down - "Page Scroll" - Long-pressing scrolls a page up/down
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
- **Short Power Button Click**: Controls the effect of a short click of the power button: - **Short Power Button Click**: Controls the effect of a short click of the power button:
@@ -523,11 +563,14 @@ On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the dev
When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position. When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position.
If the device goes to sleep or you close the book while viewing a footnote, the book reopens to your original reading position, not the footnote.
### System Navigation ### System Navigation
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen. * **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen. * **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more. * **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Disabled" does nothing. A short press always opens the Reader Menu.
### Supported Languages ### Supported Languages
@@ -574,9 +617,9 @@ Accessible by selecting **Chapters** from the Reader Menu.
Bookmarks can be created to quickly save and restore your place in a book. Bookmarks can be created to quickly save and restore your place in a book.
To create a bookmark, hold **Confirm** for 1 second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds. To create a bookmark, hold **Confirm** for about half a second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds.
To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for 1 second, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel. To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for about 0.7 seconds, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel.
Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format. Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format.
+2 -2
View File
@@ -4,7 +4,7 @@
.DESCRIPTION .DESCRIPTION
Formats all C/C++ source and header files in the repository, excluding Formats all C/C++ source and header files in the repository, excluding
generated, vendored, and build directories (open-x4-sdk, builtinFonts, generated, vendored, and build directories (freeink-sdk, builtinFonts,
hyphenation tries, uzlib, .pio, *.generated.h). hyphenation tries, uzlib, .pio, *.generated.h).
The clang-format binary path is resolved once and cached in The clang-format binary path is resolved once and cached in
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
$clangFormat = Resolve-ClangFormat $clangFormat = Resolve-ClangFormat
$exclude = @( $exclude = @(
'open-x4-sdk' 'freeink-sdk'
'lib\EpdFont\builtinFonts' 'lib\EpdFont\builtinFonts'
'lib\Epub\Epub\hyphenation\generated' 'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib' 'lib\uzlib'
+3 -3
View File
@@ -8,7 +8,7 @@ At a high level, it is firmware that uses an activity-driven application archite
```mermaid ```mermaid
graph TD graph TD
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk] A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
B --> C[lib/hal wrappers] B --> C[lib/hal wrappers]
C --> D[src/main.cpp runtime loop] C --> D[src/main.cpp runtime loop]
D --> E[Activities layer] D --> E[Activities layer]
@@ -195,10 +195,10 @@ When editing related source assets, regenerate via normal build steps/scripts.
- `src/`: app orchestration, settings/state, and activity implementations - `src/`: app orchestration, settings/state, and activity implementations
- `src/network/`: web server and OTA/update networking - `src/network/`: web server and OTA/update networking
- `src/components/`: theming and shared UI components - `src/components/`: theming and shared UI components
- `lib/hal/`: hardware abstraction wrappers around open-x4-sdk - `lib/hal/`: hardware abstraction wrappers around freeink-sdk
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation - `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.) - `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery) - `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
- `docs/`: user and technical documentation - `docs/`: user and technical documentation
## Embedded constraints that shape design ## Embedded constraints that shape design
+5 -3
View File
@@ -90,13 +90,13 @@ if (parsedSize != fileSize) {
## `section.bin` ## `section.bin`
### Version 25 ### Version 28
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 25 includes: Version 28 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
@@ -105,6 +105,8 @@ Version 25 includes:
- paragraph and list-item LUTs used by KOReader sync page refinement - paragraph and list-item LUTs used by KOReader sync page refinement
- optional per-word Focus Reading split metadata - optional per-word Focus Reading split metadata
- per-page footnote entries - per-page footnote entries
- serialized word style bits for underline, strikethrough, superscript, and
subscript
ImHex pattern: ImHex pattern:
@@ -113,7 +115,7 @@ import std.mem;
import std.string; import std.string;
import std.core; import std.core;
#define EXPECTED_VERSION 25 #define EXPECTED_VERSION 28
#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
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Submodule
+1
Submodule freeink-sdk added at f611d71d8e
+3
View File
@@ -101,6 +101,9 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
} }
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const { int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
if (utf8IsCjkBreakable(leftCp) || utf8IsCjkBreakable(rightCp)) {
return 0;
}
if (!data->kernMatrix) { if (!data->kernMatrix) {
return 0; return 0;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
#include "EpdFontFamily.h" #include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const { const EpdFont* EpdFontFamily::getFont(const Style style) const {
// Extract font style bits (ignore UNDERLINE bit for font selection) // Extract font style bits; render-time overlay bits do not affect font selection.
const bool hasBold = (style & BOLD) != 0; const bool hasBold = (style & BOLD) != 0;
const bool hasItalic = (style & ITALIC) != 0; const bool hasItalic = (style & ITALIC) != 0;
+4
View File
@@ -17,6 +17,7 @@ class EpdFontFamily {
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
}; };
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr, explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr) const EpdFont* boldItalic = nullptr)
@@ -27,6 +28,9 @@ class EpdFontFamily {
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const; const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const; int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const; uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
static constexpr bool hasTextDecoration(const Style style) {
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
}
private: private:
const EpdFont* regular; const EpdFont* regular;
+81 -41
View File
@@ -115,6 +115,9 @@ void SdCardFont::freeStyleAll(PerStyle& s) {
freeStyleMiniData(s); freeStyleMiniData(s);
delete[] s.fullIntervals; delete[] s.fullIntervals;
s.fullIntervals = nullptr; s.fullIntervals = nullptr;
delete[] s.bmpIntervals;
s.bmpIntervals = nullptr;
s.intervalsAreBmp16 = false;
freeStyleKernLigatureData(s); freeStyleKernLigatureData(s);
s.present = false; s.present = false;
} }
@@ -516,59 +519,94 @@ bool SdCardFont::load(const char* path) {
styleCount_ = styleCount; styleCount_ = styleCount;
contentHash_ = hash; contentHash_ = hash;
// Load full intervals into RAM for each present style // Load full intervals into RAM for each present style. BMP-only fonts with
// fewer than 65536 glyphs use a compact 6-byte interval table instead of the
// on-disk 12-byte table; large sparse CJK subsets otherwise keep tens of KB
// of always-resident heap just for lookup metadata.
for (uint8_t i = 0; i < MAX_STYLES; i++) { for (uint8_t i = 0; i < MAX_STYLES; i++) {
auto& s = styles_[i]; auto& s = styles_[i];
if (!s.present) continue; if (!s.present) continue;
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
if (!s.fullIntervals) {
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
freeAll();
return false;
}
if (!file.seekSet(s.intervalsFileOffset)) { if (!file.seekSet(s.intervalsFileOffset)) {
LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i); LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i);
freeAll(); freeAll();
return false; return false;
} }
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
freeAll();
return false;
}
// Validate interval contents before any later code (findGlobalGlyphIndex, // Validate interval contents before any later code (findGlobalGlyphIndex,
// glyph reads) trusts them. A malformed file could otherwise drive // glyph reads) trusts them. A malformed file could otherwise drive
// out-of-range glyph indices into bogus on-disk reads. // out-of-range glyph indices into bogus on-disk reads.
{ bool canUseBmp16 = s.header.glyphCount <= UINT16_MAX;
uint32_t expectedOffset = 0; uint32_t expectedOffset = 0;
uint32_t prevLast = 0; uint32_t prevLast = 0;
EpdUnicodeInterval iv{};
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
LOG_ERR("SDCF", "Failed to read interval %u for style %u", j, i);
freeAll();
return false;
}
if (iv.first > iv.last) {
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
file.close();
freeAll();
return false;
}
const uint32_t span = iv.last - iv.first + 1;
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
const bool spanTooBig = (span > s.header.glyphCount);
const bool offsetMismatch = (iv.offset != expectedOffset);
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
overlapsPrev, span, offsetMismatch, offsetOverruns);
file.close();
freeAll();
return false;
}
if (iv.first > UINT16_MAX || iv.last > UINT16_MAX || iv.offset > UINT16_MAX) {
canUseBmp16 = false;
}
expectedOffset += span;
prevLast = iv.last;
}
if (!file.seekSet(s.intervalsFileOffset)) {
LOG_ERR("SDCF", "Failed to seek back to intervals for style %u", i);
freeAll();
return false;
}
if (canUseBmp16) {
s.bmpIntervals = new (std::nothrow) PerStyle::BmpInterval16[s.header.intervalCount];
if (!s.bmpIntervals) {
LOG_ERR("SDCF", "Failed to allocate compact intervals for style %u", i);
freeAll();
return false;
}
for (uint32_t j = 0; j < s.header.intervalCount; ++j) { for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
const auto& iv = s.fullIntervals[j]; if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
if (iv.first > iv.last) { LOG_ERR("SDCF", "Failed to read compact interval %u for style %u", j, i);
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
file.close();
freeAll(); freeAll();
return false; return false;
} }
const uint32_t span = iv.last - iv.first + 1; s.bmpIntervals[j] = {static_cast<uint16_t>(iv.first), static_cast<uint16_t>(iv.last),
const bool overlapsPrev = (j > 0 && iv.first <= prevLast); static_cast<uint16_t>(iv.offset)};
const bool spanTooBig = (span > s.header.glyphCount); }
const bool offsetMismatch = (iv.offset != expectedOffset); s.intervalsAreBmp16 = true;
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span); } else {
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) { s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j, if (!s.fullIntervals) {
overlapsPrev, span, offsetMismatch, offsetOverruns); LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
file.close(); freeAll();
freeAll(); return false;
return false; }
} size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
expectedOffset += span; if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
prevLast = iv.last; LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
freeAll();
return false;
} }
} }
@@ -603,13 +641,15 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)
int right = static_cast<int>(s.header.intervalCount) - 1; int right = static_cast<int>(s.header.intervalCount) - 1;
while (left <= right) { while (left <= right) {
int mid = left + (right - left) / 2; int mid = left + (right - left) / 2;
const auto& interval = s.fullIntervals[mid]; const uint32_t first = s.intervalsAreBmp16 ? s.bmpIntervals[mid].first : s.fullIntervals[mid].first;
if (codepoint < interval.first) { const uint32_t last = s.intervalsAreBmp16 ? s.bmpIntervals[mid].last : s.fullIntervals[mid].last;
if (codepoint < first) {
right = mid - 1; right = mid - 1;
} else if (codepoint > interval.last) { } else if (codepoint > last) {
left = mid + 1; left = mid + 1;
} else { } else {
return static_cast<int32_t>(interval.offset + (codepoint - interval.first)); const uint32_t offset = s.intervalsAreBmp16 ? s.bmpIntervals[mid].offset : s.fullIntervals[mid].offset;
return static_cast<int32_t>(offset + (codepoint - first));
} }
} }
return -1; return -1;
@@ -1257,7 +1297,7 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr; if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr;
const auto& s = self->styles_[styleIdx]; const auto& s = self->styles_[styleIdx];
if (!s.fullIntervals) return nullptr; if (!s.fullIntervals && !s.bmpIntervals) return nullptr;
// Check overflow cache first (matching both codepoint and style) // Check overflow cache first (matching both codepoint and style)
for (uint32_t i = 0; i < self->overflowCount_; i++) { for (uint32_t i = 0; i < self->overflowCount_; i++) {
+11 -3
View File
@@ -58,9 +58,9 @@ class SdCardFont {
// Returns true if advance table is populated for at least one style. // Returns true if advance table is populated for at least one style.
bool hasAdvanceTable() const; bool hasAdvanceTable() const;
// Free mini data for all styles, restore stub EpdFontData. // Free mini data for all styles and restore stub EpdFontData.
// Also clears the temporary advance table (built per layout pass) but // Preserves the persistent advance cache so repeated layout passes can reuse
// preserves the persistent advance cache (reused across passes). // previously fetched metrics.
void clearCache(); void clearCache();
// Drop the persistent advance cache. Call when unloading the SD font or // Drop the persistent advance cache. Call when unloading the SD font or
@@ -140,6 +140,14 @@ 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;
struct BmpInterval16 {
uint16_t first;
uint16_t last;
uint16_t offset;
} __attribute__((packed));
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
BmpInterval16* bmpIntervals = nullptr;
bool intervalsAreBmp16 = false;
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm). // Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
// The full kern MATRIX is NOT resident — on Literata-class fonts a single // The full kern MATRIX is NOT resident — on Literata-class fonts a single
+5 -9
View File
@@ -34,19 +34,15 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender
unloadAll(renderer); unloadAll(renderer);
} }
// Select by ordinal position: sort available sizes, then map the font size // Select the physical point size closest to the built-in reader sizes. Some
// enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the // CJK font packs only ship larger sizes, so ordinal selection can make
// family has fewer sizes than 4, clamp to the last available size. // MEDIUM load 18pt+ and produce oversized pages on small devices.
auto sizes = family.availableSizes(); const SdCardFontFileInfo* selected = family.findClosestReaderSize(fontSizeEnum);
if (sizes.empty()) { if (!selected) {
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str()); LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
return false; return false;
} }
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
auto* font = new (std::nothrow) SdCardFont(); auto* font = new (std::nothrow) SdCardFont();
if (!font) { if (!font) {
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str()); LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
+5 -5
View File
@@ -15,10 +15,10 @@ class SdCardFontManager {
SdCardFontManager(const SdCardFontManager&) = delete; SdCardFontManager(const SdCardFontManager&) = delete;
SdCardFontManager& operator=(const SdCardFontManager&) = delete; SdCardFontManager& operator=(const SdCardFontManager&) = delete;
// Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by // Load the font file whose physical point size is closest to the reader
// ordinal position in the family's sorted size list. Only one .cpfont file // fontSizeEnum (SMALL=12, MEDIUM=14, LARGE=16, EXTRA_LARGE=18). Only one
// is loaded; other sizes remain on disk. This keeps resident interval + // .cpfont file is loaded; other sizes remain on disk. This keeps resident
// kern/ligature tables to one size's worth of memory. // interval + kern/ligature tables to one size's worth of memory.
// Returns true on success. // Returns true on success.
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum); bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
@@ -32,7 +32,7 @@ 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_; };
// Point size that was actually loaded (closest match to targetPtSize). // 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_; };
+54
View File
@@ -15,6 +15,60 @@ const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t s
return nullptr; return nullptr;
} }
const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum,
const uint8_t style) const {
if (files.empty()) return nullptr;
// Collect sizes matching the requested style, sorted ascending.
std::vector<uint8_t> sizes;
for (const auto& f : files) {
if (f.style != style) continue;
sizes.push_back(f.pointSize);
}
if (sizes.empty()) return nullptr;
std::sort(sizes.begin(), sizes.end());
// When the family provides at least 4 sizes, use ordinal (index-based)
// selection so custom-built font sets (e.g. 10/12/14/16) map SMALL to
// the smallest file, not to a hardcoded 12pt target.
if (sizes.size() >= 4) {
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
return findFile(sizes[idx], style);
}
// Fewer sizes than enum slots (e.g. CJK packs with only 2-3 sizes):
// fall back to closest-match against the built-in reader targets.
uint8_t target = 14;
switch (fontSizeEnum) {
case 0:
target = 12;
break;
case 2:
target = 16;
break;
case 3:
target = 18;
break;
case 1:
default:
target = 14;
break;
}
const SdCardFontFileInfo* best = nullptr;
uint8_t bestDelta = 255;
for (const auto& f : files) {
if (f.style != style) continue;
const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize;
if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) {
best = &f;
bestDelta = delta;
}
}
return best;
}
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const { bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
for (const auto& f : files) { for (const auto& f : files) {
if (f.pointSize == size) return true; if (f.pointSize == size) return true;
+1
View File
@@ -18,6 +18,7 @@ struct SdCardFontFamilyInfo {
std::vector<SdCardFontFileInfo> files; std::vector<SdCardFontFileInfo> files;
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const; const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
const SdCardFontFileInfo* findClosestReaderSize(uint8_t fontSizeEnum, uint8_t style = 0) const;
bool hasSize(uint8_t size) const; bool hasSize(uint8_t size) const;
std::vector<uint8_t> availableSizes() const; std::vector<uint8_t> availableSizes() const;
}; };
+35 -7
View File
@@ -33,6 +33,7 @@ import sys
import tempfile import tempfile
import threading import threading
import time import time
import socket
import urllib.request import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
@@ -49,17 +50,44 @@ INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf" DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
def download_font(url: str, dest: Path) -> Path: _orig_getaddrinfo = socket.getaddrinfo
"""Download a font file if not already cached. Returns the local path."""
def _ipv4_only_getaddrinfo(*args, **kwargs):
"""getaddrinfo variant that drops AAAA records (IPv4 only)."""
return [ai for ai in _orig_getaddrinfo(*args, **kwargs) if ai[0] == socket.AF_INET]
def download_font(url: str, dest: Path, retries: int = 3) -> Path:
"""Download a font file if not already cached. Returns the local path.
Some sources (e.g. mirrors.ctan.org) are round-robin redirectors that land
on a different mirror each request; a mirror may advertise an IPv6 address a
host without an IPv6 route cannot reach ([Errno 101] Network is unreachable).
Retry on failure, forcing IPv4 resolution after the first attempt.
"""
if dest.exists(): if dest.exists():
return dest return dest
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
print(f" Downloading {dest.name}...") print(f" Downloading {dest.name}...")
try: last_err = None
urllib.request.urlretrieve(url, dest) for attempt in range(1, retries + 1):
except Exception as e: force_ipv4 = attempt > 1
dest.unlink(missing_ok=True) if force_ipv4:
raise RuntimeError(f"Failed to download {url}: {e}") from e socket.getaddrinfo = _ipv4_only_getaddrinfo
try:
urllib.request.urlretrieve(url, dest)
break
except Exception as e: # noqa: BLE001 - reported via RuntimeError below
last_err = e
dest.unlink(missing_ok=True)
if attempt < retries:
print(f" Attempt {attempt} failed ({e}); retrying (IPv4-only)...")
finally:
if force_ipv4:
socket.getaddrinfo = _orig_getaddrinfo
else:
raise RuntimeError(f"Failed to download {url}: {last_err}") from last_err
size_kb = dest.stat().st_size / 1024 size_kb = dest.stat().st_size / 1024
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)") print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
return dest return dest
+1 -1
View File
@@ -248,7 +248,7 @@ unmerged_intervals = sorted(intervals + add_ints)
intervals = [] intervals = []
unvalidated_intervals = [] unvalidated_intervals = []
for i_start, i_end in unmerged_intervals: for i_start, i_end in unmerged_intervals:
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]: if len(unvalidated_intervals) > 0 and i_start <= unvalidated_intervals[-1][1] + 1:
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end)) unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
continue continue
unvalidated_intervals.append((i_start, i_end)) unvalidated_intervals.append((i_start, i_end))
+3 -2
View File
@@ -40,7 +40,8 @@ INTERVAL_PRESETS = {
"ascii": [(0x0020, 0x007E)], "ascii": [(0x0020, 0x007E)],
"latin1": [(0x0080, 0x00FF)], "latin1": [(0x0080, 0x00FF)],
"latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F), "latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F),
(0x1E00, 0x1EFF), (0x2000, 0x206F), (0xFB00, 0xFB06)], (0x02B0, 0x02FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
(0xFB00, 0xFB06)],
"greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)], "greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)],
"cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)], "cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)],
"hebrew": [(0x0590, 0x05FF), (0xFB1D, 0xFB4F)], "hebrew": [(0x0590, 0x05FF), (0xFB1D, 0xFB4F)],
@@ -62,7 +63,7 @@ INTERVAL_PRESETS = {
# Composite preset for English-language literary fiction including scifi/popsci. # Composite preset for English-language literary fiction including scifi/popsci.
# Greek for physics terms, math operators, geometric shapes, uncommon # Greek for physics terms, math operators, geometric shapes, uncommon
# dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats. # dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats.
"reading": [(0x0020, 0x024F), (0x0300, 0x036F), (0x0370, 0x03FF), "reading": [(0x0020, 0x024F), (0x02B0, 0x02FF), (0x0300, 0x036F), (0x0370, 0x03FF),
(0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F), (0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
(0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F), (0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F),
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F), (0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
+10 -1
View File
@@ -132,7 +132,16 @@ families:
bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"} bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"}
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"} italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"} bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
- name: Vollkorn
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 700}}
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 400}}
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
# ── Sans-serif ───────────────────────────────────────────────────────── # ── Sans-serif ─────────────────────────────────────────────────────────
+4 -2
View File
@@ -5,6 +5,7 @@
#include <JpegToBmpConverter.h> #include <JpegToBmpConverter.h>
#include <Logging.h> #include <Logging.h>
#include <PngToBmpConverter.h> #include <PngToBmpConverter.h>
#include <Utf8.h>
#include <ZipFile.h> #include <ZipFile.h>
#include "Epub/parsers/ContainerParser.h" #include "Epub/parsers/ContainerParser.h"
@@ -73,8 +74,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const
return false; return false;
} }
// Grab data from opfParser into epub // Grab data from opfParser into epub. Normalize titles to NFC so NFD (combining
bookMetadata.title = opfParser.title; // mark) text renders correctly — the device fonts have no mark positioning.
bookMetadata.title = utf8ComposeNfc(opfParser.title);
bookMetadata.author = opfParser.author; bookMetadata.author = opfParser.author;
bookMetadata.language = opfParser.language; bookMetadata.language = opfParser.language;
bookMetadata.coverItemHref = opfParser.coverItemHref; bookMetadata.coverItemHref = opfParser.coverItemHref;
+5 -2
View File
@@ -2,6 +2,7 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <Utf8.h>
#include <ZipFile.h> #include <ZipFile.h>
#include <deque> #include <deque>
@@ -9,7 +10,7 @@
#include "FsHelpers.h" #include "FsHelpers.h"
namespace { namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-composed
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";
@@ -364,7 +365,9 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
} }
} }
const TocEntry entry(title, href, anchor, level, spineIndex); // 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.
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
writeTocEntry(tocFile, entry); writeTocEntry(tocFile, entry);
tocCount++; tocCount++;
} }
+20 -3
View File
@@ -6,6 +6,20 @@
#include <new> #include <new>
namespace {
template <typename Predicate>
void renderFilteredPageElements(const std::vector<std::shared_ptr<PageElement>>& elements, GfxRenderer& renderer,
const int fontId, const int xOffset, const int yOffset, Predicate&& predicate) {
for (const auto& element : elements) {
if (predicate(*element)) {
element->render(renderer, fontId, xOffset, yOffset);
}
}
}
} // namespace
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
} }
@@ -93,9 +107,12 @@ std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(HalFile& fil
} }
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
for (auto& element : elements) { renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, [](const PageElement&) { return true; });
element->render(renderer, fontId, xOffset, yOffset); }
}
void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset,
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
} }
bool Page::serialize(HalFile& file) const { bool Page::serialize(HalFile& file) const {
+1
View File
@@ -88,6 +88,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;
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);
+259 -35
View File
@@ -24,6 +24,7 @@ constexpr size_t RTL_PARAGRAPH_PROBE_WORDS = 3;
// Per-word: scan enough chars to see through leading neutrals (quotes, numbers) // Per-word: scan enough chars to see through leading neutrals (quotes, numbers)
// before giving up. 64 is a hedge for pathological cases like long numeric tokens. // before giving up. 64 is a hedge for pathological cases like long numeric tokens.
constexpr int RTL_PER_WORD_PROBE_DEPTH = 64; constexpr int RTL_PER_WORD_PROBE_DEPTH = 64;
constexpr size_t MIN_JUSTIFY_GAPS = 1;
// Byte-level pre-check: Hebrew UTF-8 lead bytes 0xD6-0xD7, Arabic/Syriac 0xD8-0xDB. // Byte-level pre-check: Hebrew UTF-8 lead bytes 0xD6-0xD7, Arabic/Syriac 0xD8-0xDB.
bool mayContainRtlBytes(const char* str) { bool mayContainRtlBytes(const char* str) {
@@ -57,6 +58,134 @@ uint32_t lastCodepoint(const std::string& word) {
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; } bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
bool isNoBreakBeforeCjkPunctuation(const uint32_t cp) {
switch (cp) {
case '.':
case ',':
case ':':
case ';':
case '!':
case '?':
case ')':
case ']':
case '}':
case 0x00BB: // »
case 0x2019: //
case 0x201D: // ”
case 0x3001: // 、
case 0x3002: // 。
case 0x3009: // 〉
case 0x300B: // 》
case 0x300D: // 」
case 0x300F: // 』
case 0x3011: // 】
case 0x3015: //
case 0x3017: // 〗
case 0x3019: // 〙
case 0x301B: // 〛
case 0xFF01: //
case 0xFF09: //
case 0xFF0C: //
case 0xFF0E: //
case 0xFF1A: //
case 0xFF1B: //
case 0xFF1F: //
case 0xFF3D: //
case 0xFF5D: //
return true;
default:
return false;
}
}
bool isNoBreakAfterCjkPunctuation(const uint32_t cp) {
switch (cp) {
case '(':
case '[':
case '{':
case 0x00AB: // «
case 0x2018: //
case 0x201C: // “
case 0x3008: // 〈
case 0x300A: // 《
case 0x300C: // 「
case 0x300E: // 『
case 0x3010: // 【
case 0x3014: //
case 0x3016: // 〖
case 0x3018: // 〘
case 0x301A: // 〚
case 0xFF08: //
case 0xFF3B: //
case 0xFF5B: //
return true;
default:
return false;
}
}
bool containsCjkBreakableCodepoint(const std::string& text) {
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
while (*ptr) {
const uint32_t cp = utf8NextCodepoint(&ptr);
if (utf8IsCjkBreakable(cp)) {
return true;
}
}
return false;
}
bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) {
if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false;
if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false;
if (utf8IsCombiningMark(rightCp)) return false;
return true;
}
std::vector<size_t> cjkCharacterBreakByteOffsets(const std::string& text) {
struct CodepointBoundary {
uint32_t cp;
size_t endOffset;
};
std::vector<CodepointBoundary> codepoints;
codepoints.reserve(text.size());
bool hasCjkBreakable = false;
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
const auto* const start = ptr;
while (*ptr) {
const uint32_t cp = utf8NextCodepoint(&ptr);
if (cp == 0) break;
if (utf8IsCjkBreakable(cp)) {
hasCjkBreakable = true;
}
codepoints.push_back({cp, static_cast<size_t>(ptr - start)});
}
if (!hasCjkBreakable || codepoints.size() < 2) return {};
std::vector<size_t> allowedOffsets;
allowedOffsets.reserve(codepoints.size() - 1);
for (size_t i = 0; i + 1 < codepoints.size(); ++i) {
const uint32_t current = codepoints[i].cp;
const uint32_t next = codepoints[i + 1].cp;
if (!hasCjkBreakOpportunityBetween(current, next)) continue;
allowedOffsets.push_back(codepoints[i].endOffset);
}
return allowedOffsets;
}
int computeJustifyExtra(const int spareSpace, const size_t gapCount) {
if (gapCount < MIN_JUSTIFY_GAPS || spareSpace <= 0) return 0;
// Distribute the spare space evenly across gaps. Do NOT bail out to 0 when the
// per-gap stretch is large: a sparse line (few words on a wide page) legitimately
// needs big gaps to reach the margin. Returning 0 there disables justification for
// that line, leaving it right-aligned (RTL) / left-aligned (LTR) — the mismatched
// alignment bug. Match the un-capped behavior of the old code.
return spareSpace / static_cast<int>(gapCount);
}
// Removes every soft hyphen in-place so rendered glyphs match measured widths. // Removes every soft hyphen in-place so rendered glyphs match measured widths.
void stripSoftHyphensInPlace(std::string& word) { void stripSoftHyphensInPlace(std::string& word) {
size_t pos = 0; size_t pos = 0;
@@ -125,6 +254,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
const bool attachToPrevious) { const bool attachToPrevious) {
if (word.empty()) return; if (word.empty()) return;
// The device fonts carry no combining-mark positioning, so EPUB text stored in NFD
// (a base letter followed by separate combining accents -- common for Vietnamese,
// and used for many EPUB <h1> chapter headings) renders with the marks detached or
// misplaced. Compose to NFC here, the single funnel every word passes through, so a
// precomposed glyph is used instead. This runs once per word at layout time (the
// result is cached in the section file) and is a cheap no-op for mark-free text.
word = utf8ComposeNfc(word);
EpdFontFamily::Style baseStyle = fontStyle; EpdFontFamily::Style baseStyle = fontStyle;
if (underline) { if (underline) {
baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE); baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE);
@@ -132,12 +269,54 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) && const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) &&
BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH); BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH);
const auto pushToken = [&](std::string token, const bool continues, const bool noSpaceBefore,
const bool isFocusSuffix) {
words.push_back(std::move(token));
wordStyles.push_back(baseStyle);
wordContinues.push_back(continues);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(isFocusSuffix);
};
bool effectiveAttachToPrevious = attachToPrevious;
bool effectiveNoSpaceBefore = false;
if (attachToPrevious && !words.empty() &&
hasCjkBreakOpportunityBetween(lastCodepoint(words.back()), firstCodepoint(word))) {
effectiveAttachToPrevious = false;
effectiveNoSpaceBefore = true;
}
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
bool firstToken = true;
size_t tokenStart = 0;
for (const size_t breakOffset : breakOffsets) {
if (breakOffset <= tokenStart || breakOffset > word.size()) continue;
pushToken(word.substr(tokenStart, breakOffset - tokenStart), firstToken ? effectiveAttachToPrevious : false,
firstToken ? effectiveNoSpaceBefore : true, false);
firstToken = false;
tokenStart = breakOffset;
}
if (tokenStart < word.size()) {
pushToken(word.substr(tokenStart), firstToken ? effectiveAttachToPrevious : false,
firstToken ? effectiveNoSpaceBefore : true, false);
}
if (wordStartsRtl) {
hasRtlWord = true;
}
return;
}
if (containsCjkBreakableCodepoint(word)) {
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
if (wordStartsRtl) {
hasRtlWord = true;
}
return;
}
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later. // Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) { if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
words.push_back(std::move(word)); pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
wordStyles.push_back(baseStyle);
wordContinues.push_back(attachToPrevious);
wordIsFocusSuffix.push_back(false);
if (wordStartsRtl) { if (wordStartsRtl) {
hasRtlWord = true; hasRtlWord = true;
} }
@@ -166,17 +345,19 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
words.reserve(newCapacity); words.reserve(newCapacity);
wordStyles.reserve(newCapacity); wordStyles.reserve(newCapacity);
wordContinues.reserve(newCapacity); wordContinues.reserve(newCapacity);
wordNoSpaceBefore.reserve(newCapacity);
wordIsFocusSuffix.reserve(newCapacity); wordIsFocusSuffix.reserve(newCapacity);
} }
// Lambda helper to process and push individual sub-segments of the string // Lambda helper to process and push individual sub-segments of the string
// Use std::string_view to avoid heap allocations when slicing // Use std::string_view to avoid heap allocations when slicing
auto processSegment = [&](std::string_view segment, bool isWord, bool attach) { auto processSegment = [&](std::string_view segment, bool isWord, bool attach, bool noSpaceBefore) {
if (!isWord) { if (!isWord) {
// Punctuation and Numbers stay regular // Punctuation and Numbers stay regular
words.emplace_back(segment); words.emplace_back(segment);
wordStyles.push_back(baseStyle); wordStyles.push_back(baseStyle);
wordContinues.push_back(attach); wordContinues.push_back(attach);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(false); wordIsFocusSuffix.push_back(false);
} else { } else {
size_t charCount = 0; size_t charCount = 0;
@@ -198,6 +379,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
words.emplace_back(segment); words.emplace_back(segment);
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD)); wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach); wordContinues.push_back(attach);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(false); wordIsFocusSuffix.push_back(false);
} else { } else {
countPtr = reinterpret_cast<const unsigned char*>(segment.data()); countPtr = reinterpret_cast<const unsigned char*>(segment.data());
@@ -210,12 +392,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
words.emplace_back(segment.substr(0, splitByteOffset)); words.emplace_back(segment.substr(0, splitByteOffset));
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD)); wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
wordContinues.push_back(attach); wordContinues.push_back(attach);
wordNoSpaceBefore.push_back(noSpaceBefore);
wordIsFocusSuffix.push_back(false); wordIsFocusSuffix.push_back(false);
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry // Regular suffix - marked so extractLine can merge it back into single TextBlock entry
words.emplace_back(segment.substr(splitByteOffset)); words.emplace_back(segment.substr(splitByteOffset));
wordStyles.push_back(baseStyle); wordStyles.push_back(baseStyle);
wordContinues.push_back(true); wordContinues.push_back(true);
wordNoSpaceBefore.push_back(false);
wordIsFocusSuffix.push_back(true); wordIsFocusSuffix.push_back(true);
} }
} }
@@ -243,7 +427,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
// Only the very first segment inherits the original attachToPrevious flag. // Only the very first segment inherits the original attachToPrevious flag.
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix. // Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true); processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
isFirstSegment ? effectiveNoSpaceBefore : false);
// Setup for the next segment // Setup for the next segment
segmentStart = currentCpStart; segmentStart = currentCpStart;
@@ -255,7 +440,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
// Process the final remaining segment // Process the final remaining segment
size_t segmentLen = end - segmentStart; size_t segmentLen = end - segmentStart;
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen); std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true); processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
isFirstSegment ? effectiveNoSpaceBefore : false);
if (wordStartsRtl) { if (wordStartsRtl) {
hasRtlWord = true; hasRtlWord = true;
} }
@@ -324,14 +510,16 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
std::vector<size_t> lineBreakIndices; std::vector<size_t> lineBreakIndices;
if (hyphenationEnabled) { if (hyphenationEnabled) {
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits. // Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues); lineBreakIndices =
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
} else { } else {
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues); lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
} }
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1; const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) { for (size_t i = 0; i < lineCount; ++i) {
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId); extractLine(i, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore, lineBreakIndices, processLine, renderer,
fontId);
} }
// Remove consumed words so size() reflects only remaining words // Remove consumed words so size() reflects only remaining words
@@ -340,6 +528,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
words.erase(words.begin(), words.begin() + consumed); words.erase(words.begin(), words.begin() + consumed);
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed); wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed); wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
wordNoSpaceBefore.erase(wordNoSpaceBefore.begin(), wordNoSpaceBefore.begin() + consumed);
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed); wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
} }
} }
@@ -356,7 +545,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
} }
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) { std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec) {
if (words.empty()) { if (words.empty()) {
return {}; return {};
} }
@@ -395,7 +585,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
for (size_t j = i; j < totalWordCount; ++j) { for (size_t j = i; j < totalWordCount; ++j) {
// Add space before word j, unless it's the first word on the line or a continuation // Add space before word j, unless it's the first word on the line or a continuation
int gap = 0; int gap = 0;
if (j > static_cast<size_t>(i) && !continuesVec[j]) { if (j > static_cast<size_t>(i) && noSpaceBeforeVec[j]) {
gap = 0;
} else if (j > static_cast<size_t>(i) && !continuesVec[j]) {
gap = gap =
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]); renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
} else if (j > static_cast<size_t>(i) && continuesVec[j]) { } else if (j > static_cast<size_t>(i) && continuesVec[j]) {
@@ -470,7 +662,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
// Builds break indices while opportunistically splitting the word that would overflow the current line. // Builds break indices while opportunistically splitting the word that would overflow the current line.
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId, std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
const int pageWidth, std::vector<uint16_t>& wordWidths, const int pageWidth, std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) { std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec) {
const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId); const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId);
std::vector<size_t> lineBreakIndices; std::vector<size_t> lineBreakIndices;
@@ -488,7 +681,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
while (currentIndex < wordWidths.size()) { while (currentIndex < wordWidths.size()) {
const bool isFirstWord = currentIndex == lineStart; const bool isFirstWord = currentIndex == lineStart;
int spacing = 0; int spacing = 0;
if (!isFirstWord && !continuesVec[currentIndex]) { if (!isFirstWord && noSpaceBeforeVec[currentIndex]) {
spacing = 0;
} else if (!isFirstWord && !continuesVec[currentIndex]) {
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]), spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]); firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
} else if (!isFirstWord && continuesVec[currentIndex]) { } else if (!isFirstWord && continuesVec[currentIndex]) {
@@ -618,6 +813,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
// line, while "kilometer" moves to the next line. // line, while "kilometer" moves to the next line.
// wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment. // wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment.
wordContinues.insert(wordContinues.begin() + wordIndex + 1, false); wordContinues.insert(wordContinues.begin() + wordIndex + 1, false);
wordNoSpaceBefore.insert(wordNoSpaceBefore.begin() + wordIndex + 1, false);
// Update cached widths to reflect the new prefix/remainder pairing. // Update cached widths to reflect the new prefix/remainder pairing.
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth); wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
@@ -627,7 +823,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
} }
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths, void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices, const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
const GfxRenderer& renderer, const int fontId) { const GfxRenderer& renderer, const int fontId) {
const size_t lineBreak = lineBreakIndices[breakIndex]; const size_t lineBreak = lineBreakIndices[breakIndex];
@@ -660,7 +857,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx]; lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
// Count gaps: each word after the first creates a gap, unless it's a continuation // Count gaps: each word after the first creates a gap, unless it's a continuation
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) { if (wordIdx > 0 && noSpaceBeforeVec[lastBreakAt + wordIdx]) {
// Unicode break opportunity with no inserted Latin-style space. It is still
// a stretchable gap for justified CJK/Korean text.
actualGapCount++;
} else if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
actualGapCount++; actualGapCount++;
totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx - 1]), totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx - 1]),
firstCodepoint(lineWords[wordIdx]), lineWordStyles[wordIdx - 1]); firstCodepoint(lineWords[wordIdx]), lineWordStyles[wordIdx - 1]);
@@ -689,8 +890,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// For justified text, compute per-gap extra to distribute remaining space evenly // For justified text, compute per-gap extra to distribute remaining space evenly
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1) const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? spareSpace / static_cast<int>(actualGapCount) ? computeJustifyExtra(spareSpace, actualGapCount)
: 0; : 0;
// BiDi processing: reorder words with UAX#9 in full-line context. // BiDi processing: reorder words with UAX#9 in full-line context.
@@ -709,11 +910,13 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
reorderedStylesScratch.clear(); reorderedStylesScratch.clear();
reorderedWidthsScratch.clear(); reorderedWidthsScratch.clear();
reorderedContinuesScratch.clear(); reorderedContinuesScratch.clear();
reorderedNoSpaceBeforeScratch.clear();
reorderedFocusSuffixScratch.clear(); reorderedFocusSuffixScratch.clear();
reorderedWordsScratch.reserve(visualOrderScratch.size()); reorderedWordsScratch.reserve(visualOrderScratch.size());
reorderedStylesScratch.reserve(visualOrderScratch.size()); reorderedStylesScratch.reserve(visualOrderScratch.size());
reorderedWidthsScratch.reserve(visualOrderScratch.size()); reorderedWidthsScratch.reserve(visualOrderScratch.size());
reorderedContinuesScratch.reserve(visualOrderScratch.size()); reorderedContinuesScratch.reserve(visualOrderScratch.size());
reorderedNoSpaceBeforeScratch.reserve(visualOrderScratch.size());
reorderedFocusSuffixScratch.reserve(visualOrderScratch.size()); reorderedFocusSuffixScratch.reserve(visualOrderScratch.size());
for (size_t i = 0; i < visualOrderScratch.size(); ++i) { for (size_t i = 0; i < visualOrderScratch.size(); ++i) {
@@ -740,6 +943,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
} }
} }
reorderedContinuesScratch.push_back(continues); reorderedContinuesScratch.push_back(continues);
reorderedNoSpaceBeforeScratch.push_back(!continues && noSpaceBeforeVec[lastBreakAt + src]);
} }
int reorderedWordWidthSum = 0; int reorderedWordWidthSum = 0;
@@ -747,7 +951,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
int reorderedNaturalGaps = 0; int reorderedNaturalGaps = 0;
for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) { for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) {
reorderedWordWidthSum += reorderedWidthsScratch[wordIdx]; reorderedWordWidthSum += reorderedWidthsScratch[wordIdx];
if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) { if (wordIdx > 0 && reorderedNoSpaceBeforeScratch[wordIdx]) {
// Unicode break opportunity with no inserted Latin-style space. It is still
// a stretchable gap for justified CJK/Korean text.
reorderedGapCount++;
} else if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
reorderedGapCount++; reorderedGapCount++;
reorderedNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx - 1]), reorderedNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx - 1]),
firstCodepoint(reorderedWordsScratch[wordIdx]), firstCodepoint(reorderedWordsScratch[wordIdx]),
@@ -763,10 +971,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
} }
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps; const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
const int reorderedJustifyExtra = const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
(effectiveAlignment == CssTextAlign::Justify && !isLastLine && reorderedGapCount >= 1) ? computeJustifyExtra(reorderedSpare, reorderedGapCount)
? reorderedSpare / static_cast<int>(reorderedGapCount) : 0;
: 0;
const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine) const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
? reorderedJustifyExtra * static_cast<int>(reorderedGapCount) ? reorderedJustifyExtra * static_cast<int>(reorderedGapCount)
@@ -799,15 +1006,20 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
int advance = int advance =
renderer.getKerning(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]), renderer.getKerning(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
firstCodepoint(reorderedWordsScratch[wordIdx + 1]), reorderedStylesScratch[wordIdx]); firstCodepoint(reorderedWordsScratch[wordIdx + 1]), reorderedStylesScratch[wordIdx]);
if (reorderedWordsScratch[wordIdx] == " " && reorderedContinuesScratch[wordIdx] && // wordIdx > 0 mirrors the gap accounting above (which skips index 0): a leading
// no-break space must not receive justifyExtra, or the line over-stretches by one
// gap and the last word is pushed past the right margin (issue #2185).
if (wordIdx > 0 && reorderedWordsScratch[wordIdx] == " " && reorderedContinuesScratch[wordIdx] &&
effectiveAlignment == CssTextAlign::Justify && !isLastLine) { effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
advance += reorderedJustifyExtra; advance += reorderedJustifyExtra;
} }
xpos += advance; xpos += advance;
} else if (wordIdx + 1 < reorderedWidthsScratch.size()) { } else if (wordIdx + 1 < reorderedWidthsScratch.size()) {
int gap = renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]), const bool nextNoSpace = reorderedNoSpaceBeforeScratch[wordIdx + 1];
firstCodepoint(reorderedWordsScratch[wordIdx + 1]), int gap = nextNoSpace ? 0
reorderedStylesScratch[wordIdx]); : renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
reorderedStylesScratch[wordIdx]);
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) { if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
gap += reorderedJustifyExtra; gap += reorderedJustifyExtra;
} }
@@ -839,18 +1051,23 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// Cross-boundary kerning for continuation words // Cross-boundary kerning for continuation words
int advance = renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]), int advance = renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]); firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
if (lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] && // wordIdx > 0: see the LTR branch — a leading no-break space is not a justifiable gap.
if (wordIdx > 0 && lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
effectiveAlignment == CssTextAlign::Justify && !isLastLine) { effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
advance += justifyExtra; advance += justifyExtra;
} }
xpos -= advance; xpos -= advance;
} else { } else {
int gap = 0; int gap = 0;
bool nextNoSpace = false;
if (wordIdx + 1 < lineWordCount) { if (wordIdx + 1 < lineWordCount) {
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]), nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]); gap = nextNoSpace
? 0
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
} }
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) { if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
gap += justifyExtra; gap += justifyExtra;
} }
xpos -= gap; xpos -= gap;
@@ -873,18 +1090,25 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
int advance = wordWidths[lastBreakAt + wordIdx]; int advance = wordWidths[lastBreakAt + wordIdx];
advance += renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]), advance += renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]); firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
if (lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] && // wordIdx > 0 mirrors the gap accounting above (which skips index 0): a leading
// no-break space must not receive justifyExtra, or the line over-stretches by one
// gap and the last word is pushed past the right margin (issue #2185).
if (wordIdx > 0 && lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
effectiveAlignment == CssTextAlign::Justify && !isLastLine) { effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
advance += justifyExtra; advance += justifyExtra;
} }
xpos += advance; xpos += advance;
} else { } else {
int gap = 0; int gap = 0;
bool nextNoSpace = false;
if (wordIdx + 1 < lineWordCount) { if (wordIdx + 1 < lineWordCount) {
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]), nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]); gap = nextNoSpace
? 0
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
} }
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) { if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
gap += justifyExtra; gap += justifyExtra;
} }
xpos += wordWidths[lastBreakAt + wordIdx] + gap; xpos += wordWidths[lastBreakAt + wordIdx] + gap;
+9 -4
View File
@@ -15,7 +15,8 @@ class GfxRenderer;
class ParsedText { class ParsedText {
std::vector<std::string> words; std::vector<std::string> words;
std::vector<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it) std::vector<bool> wordContinues; // true = word attaches to previous with no break
std::vector<bool> wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
BlockStyle blockStyle; BlockStyle blockStyle;
bool extraParagraphSpacing; bool extraParagraphSpacing;
@@ -27,18 +28,22 @@ class ParsedText {
std::vector<EpdFontFamily::Style> reorderedStylesScratch; std::vector<EpdFontFamily::Style> reorderedStylesScratch;
std::vector<uint16_t> reorderedWidthsScratch; std::vector<uint16_t> reorderedWidthsScratch;
std::vector<bool> reorderedContinuesScratch; std::vector<bool> reorderedContinuesScratch;
std::vector<bool> reorderedNoSpaceBeforeScratch;
std::vector<bool> reorderedFocusSuffixScratch; std::vector<bool> reorderedFocusSuffixScratch;
std::vector<uint16_t> visualOrderScratch; std::vector<uint16_t> visualOrderScratch;
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const; int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec); std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec); std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
std::vector<bool>& noSpaceBeforeVec);
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks); std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths, void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices, const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer, const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
int fontId); int fontId);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId); std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
+513 -108
View File
@@ -2,6 +2,7 @@
#include <HalStorage.h> #include <HalStorage.h>
#include <Logging.h> #include <Logging.h>
#include <Memory.h>
#include <Serialization.h> #include <Serialization.h>
#include "Epub/css/CssParser.h" #include "Epub/css/CssParser.h"
@@ -10,33 +11,59 @@
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace { namespace {
constexpr uint8_t SECTION_FILE_VERSION = 26; // v28: text decoration bits now include line-through in serialized wordStyles.
constexpr uint8_t SECTION_FILE_VERSION = 28;
// Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
// as unknown and clears -- so an incomplete file is never mistaken for a valid one.
constexpr uint8_t SECTION_FILE_INCOMPLETE_VERSION = 0;
// Written when a build is suspended partway (reader exited or device slept mid-build).
// The file carries valid pages 0..pageCount-1, all LUTs, and a trailer with the parse
// watermark (bytesConsumed, totalBytes) appended after the li LUT. loadSectionFile
// accepts it so a resume shows those pages instantly; the reader extends it by
// rebuilding in the background. Uses the same header layout as SECTION_FILE_VERSION,
// so finalized files are untouched by this feature; older firmware treats the sentinel
// as an unknown version and rebuilds, which is a safe downgrade.
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t) + sizeof(uint32_t); sizeof(uint32_t) + sizeof(uint32_t);
struct PageLutEntry {
uint32_t fileOffset;
uint16_t paragraphIndex;
uint16_t listItemIndex;
};
} // namespace } // namespace
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
// constructed/destroyed where the parser's full definition is visible.
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub),
spineIndex(spineIndex),
renderer(renderer),
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
// Suspend any in-progress build so every section.reset() / navigation / sleep path
// persists the pages already laid out as a partial .bin instead of discarding them
// (no-op once a build has completed or never started).
Section::~Section() { suspendBuild(); }
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) { uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
if (!file) { if (!file) {
LOG_ERR("SCT", "File not open for writing page %d", pageCount); LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_);
return 0; return 0;
} }
const uint32_t position = file.position(); const uint32_t position = file.position();
if (!page->serialize(file)) { if (!page->serialize(file)) {
LOG_ERR("SCT", "Failed to serialize page %d", pageCount); LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_);
return 0; return 0;
} }
LOG_DBG("SCT", "Page %d processed", pageCount); LOG_DBG("SCT", "Page %d processed", builtPageCount_);
pageCount++; builtPageCount_++;
// pageCount is the pages available to read: a rebuild over a partial only raises it
// once it has laid out more pages than the partial already covers.
if (builtPageCount_ > pageCount) {
pageCount = builtPageCount_;
}
return position; return position;
} }
@@ -55,7 +82,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) + sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t), sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch"); "Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION); // Written as the incomplete sentinel; finalizeBuild() patches it to
// SECTION_FILE_VERSION as the last step, committing the file.
serialization::writePod(file, SECTION_FILE_INCOMPLETE_VERSION);
serialization::writePod(file, fontId); serialization::writePod(file, fontId);
serialization::writePod(file, lineCompression); serialization::writePod(file, lineCompression);
serialization::writePod(file, extraParagraphSpacing); serialization::writePod(file, extraParagraphSpacing);
@@ -82,16 +111,18 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
} }
// Match parameters // Match parameters
bool filePartial = false;
{ {
uint8_t version; uint8_t version;
serialization::readPod(file, version); serialization::readPod(file, version);
if (version != SECTION_FILE_VERSION) { if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) {
// Explicit close() required: member variable persists beyond function scope // Explicit close() required: member variable persists beyond function scope
file.close(); file.close();
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version); LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
clearCache(); clearCache();
return false; return false;
} }
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
int fileFontId; int fileFontId;
uint16_t fileViewportWidth, fileViewportHeight; uint16_t fileViewportWidth, fileViewportHeight;
@@ -126,14 +157,42 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
} }
serialization::readPod(file, pageCount); serialization::readPod(file, pageCount);
if (filePartial) {
// A partial's pageCount is the watermark of a suspended build. Read the watermark
// trailer (appended after the li LUT) so estimatedTotalPages can extrapolate.
uint32_t liLutOffset = 0;
file.seek(HEADER_SIZE - sizeof(uint32_t));
serialization::readPod(file, liLutOffset);
const uint32_t trailerOffset = liLutOffset + static_cast<uint32_t>(pageCount) * sizeof(uint16_t);
const bool trailerValid =
pageCount > 0 && liLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size();
if (!trailerValid) {
file.close();
LOG_ERR("SCT", "Deserialization failed: malformed partial section");
clearCache();
pageCount = 0;
return false;
}
file.seek(trailerOffset);
serialization::readPod(file, partialBytesConsumed_);
serialization::readPod(file, partialTotalBytes_);
partial_ = true;
partialPageCount_ = pageCount;
}
// Explicit close() required: member variable persists beyond function scope // Explicit close() required: member variable persists beyond function scope
file.close(); file.close();
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount); LOG_DBG("SCT", "Deserialization succeeded: %d pages%s", pageCount, filePartial ? " (partial)" : "");
return true; return true;
} }
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const { bool Section::clearCache() const {
const std::string tmpBin = binTmpPath();
if (Storage.exists(tmpBin.c_str())) {
Storage.remove(tmpBin.c_str());
}
if (!Storage.exists(filePath.c_str())) { if (!Storage.exists(filePath.c_str())) {
LOG_DBG("SCT", "Cache does not exist, no action needed"); LOG_DBG("SCT", "Cache does not exist, no action needed");
return true; return true;
@@ -153,8 +212,43 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering, const bool focusReadingEnabled, const uint8_t imageRendering, const bool focusReadingEnabled,
const std::function<void()>& popupFn) { const std::function<void()>& popupFn) {
// One-shot build: start, then lay out the whole section in a single pass.
if (!startBuild(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled, popupFn)) {
return false;
}
if (!buildSomeMore(0)) { // 0 = build to completion
return false;
}
return buildComplete_;
}
bool Section::startBuild(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight,
const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering,
const bool focusReadingEnabled, const std::function<void()>& popupFn) {
if (build_) {
LOG_ERR("SCT", "startBuild called while a build is already active");
return false;
}
buildComplete_ = false;
builtPageCount_ = 0;
// Pages from a loaded partial stay readable (from filePath) while this build writes
// to the tmp .bin, so availability never drops below the partial's watermark.
pageCount = partial_ ? partialPageCount_ : 0;
// Remove a stale tmp .bin from a crash-interrupted build; this build recreates it.
{
const std::string staleTmp = binTmpPath();
if (Storage.exists(staleTmp.c_str())) {
Storage.remove(staleTmp.c_str());
}
}
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto htmlDir = epub->getCachePath() + "/html";
const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html";
const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html";
// Create cache directory if it doesn't exist // Create cache directory if it doesn't exist
{ {
@@ -162,62 +256,101 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
Storage.mkdir(sectionsDir.c_str()); Storage.mkdir(sectionsDir.c_str());
} }
// Retry logic for SD card timing issues // Reuse the previously unzipped HTML if we already have it. The unzipped HTML is keyed only on the
bool success = false; // book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation
uint32_t fileSize = 0; // that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip
for (int attempt = 0; attempt < 3 && !success; attempt++) { // inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so
if (attempt > 0) { // even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1); // reopen skip the multi-second inflate. If htmlPath exists it is known-complete.
delay(50); // Brief delay before retry const bool reusedHtml = Storage.exists(htmlPath.c_str());
bool htmlCached = reusedHtml;
if (reusedHtml) {
LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str());
} else {
Storage.mkdir(htmlDir.c_str());
// Retry logic for SD card timing issues
bool streamed = false;
uint32_t fileSize = 0;
for (int attempt = 0; attempt < 3 && !streamed; attempt++) {
if (attempt > 0) {
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
delay(50); // Brief delay before retry
}
// Remove any incomplete file from previous attempt before retrying
if (Storage.exists(tmpHtmlPath.c_str())) {
Storage.remove(tmpHtmlPath.c_str());
}
HalFile tmpHtml;
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
continue;
}
// Larger chunks mean far fewer SD writes inflating the HTML; a 1KB chunk turned a 584KB
// single-spine novel into ~570 tiny writes (multi-second). 8KB keeps the transient buffers
// small while cutting the write count 8x.
streamed = epub->readItemContentsToStream(localPath, tmpHtml, 8192);
fileSize = tmpHtml.size();
// Explicitly close() file before calling Storage.remove()
tmpHtml.close();
// If streaming failed, remove the incomplete file immediately
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
Storage.remove(tmpHtmlPath.c_str());
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
}
} }
// Remove any incomplete file from previous attempt before retrying if (!streamed) {
if (Storage.exists(tmpHtmlPath.c_str())) { LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
Storage.remove(tmpHtmlPath.c_str()); return false;
} }
HalFile tmpHtml; LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
continue;
}
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
fileSize = tmpHtml.size();
// Explicitly close() file before calling Storage.remove()
tmpHtml.close();
// If streaming failed, remove the incomplete file immediately // Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are
if (!success && Storage.exists(tmpHtmlPath.c_str())) { // valid regardless of whether the layout build finishes, so reopening (even a window-only spine
Storage.remove(tmpHtmlPath.c_str()); // that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp.
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt"); if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) {
htmlCached = true;
} else {
LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp");
} }
} }
if (!success) { if (!Storage.openFileForWrite("SCT", binTmpPath(), file)) {
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries"); if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
return false;
}
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
if (!Storage.openFileForWrite("SCT", filePath, file)) {
return false; return false;
} }
// Header is written with the incomplete-version sentinel; finalizeBuild() commits it.
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled); viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
std::vector<PageLutEntry> lut = {};
auto ctx = makeUniqueNoThrow<BuildContext>();
if (!ctx) {
LOG_ERR("SCT", "OOM: BuildContext");
file.close();
Storage.remove(binTmpPath().c_str());
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
return false;
}
// htmlCached == "htmlPath is the live cache" (reused, or just promoted). finalizeBuild/abandonBuild
// then leave the cached HTML alone; only an un-promoted temp (rename failed) is theirs to clean up.
ctx->reusedHtml = htmlCached;
ctx->htmlPath = htmlPath;
ctx->tmpHtmlPath = tmpHtmlPath;
ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath;
// Derive the content base directory and image cache path prefix for the parser // Derive the content base directory and image cache path prefix for the parser
size_t lastSlash = localPath.find_last_of('/'); const size_t lastSlash = localPath.find_last_of('/');
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : ""; ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_"; ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
CssParser* cssParser = nullptr;
if (embeddedStyle) { if (embeddedStyle) {
cssParser = epub->getCssParser(); ctx->cssParser = epub->getCssParser();
if (cssParser) { if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
if (!cssParser->loadFromCache()) { LOG_ERR("SCT", "Failed to load CSS from cache");
LOG_ERR("SCT", "Failed to load CSS from cache");
}
} }
} }
@@ -234,104 +367,367 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
} }
} }
ChapterHtmlSlimParser visitor( // The parser stores the path/contentBase/imageBasePath by reference, so they must
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, // live in the BuildContext (which outlives the parser). The page-complete callback
viewportHeight, hyphenationEnabled, focusReadingEnabled, // captures the BuildContext pointer to append to its in-RAM LUT; build_ owns the
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) { // context for the parser's whole lifetime.
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex}); BuildContext* ctxPtr = ctx.get();
ctx->parser = makeUniqueNoThrow<ChapterHtmlSlimParser>(
epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
viewportWidth, viewportHeight, hyphenationEnabled, focusReadingEnabled,
[this, ctxPtr](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
ctxPtr->lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
}, },
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser); embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn,
Hyphenator::setPreferredLanguage(epub->getLanguage()); ctxPtr->cssParser);
success = visitor.parseAndBuildPages(); if (!ctx->parser) {
LOG_ERR("SCT", "OOM: ChapterHtmlSlimParser");
Storage.remove(tmpHtmlPath.c_str()); if (ctx->cssParser) ctx->cssParser->clear();
if (!success) {
LOG_ERR("SCT", "Failed to parse XML and build pages");
// Explicitly close() file before calling Storage.remove()
file.close(); file.close();
Storage.remove(filePath.c_str()); Storage.remove(binTmpPath().c_str());
if (cssParser) { if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
cssParser->clear();
}
return false; return false;
} }
Hyphenator::setPreferredLanguage(epub->getLanguage());
build_ = std::move(ctx);
if (!build_->parser->beginParse()) {
LOG_ERR("SCT", "Failed to begin parse");
abandonBuild();
return false;
}
build_->totalBytes = build_->parser->parseTotalBytes();
return true;
}
bool Section::buildSomeMore(const int maxPages) {
if (!build_ || !build_->parser) {
LOG_ERR("SCT", "buildSomeMore with no active build");
return false;
}
// Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial,
// pageCount stays pinned at the partial's watermark until the build passes it, which
// would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark.
const int startCount = builtPageCount_;
for (;;) {
const auto status = build_->parser->parseStep();
if (status == ChapterHtmlSlimParser::ParseStatus::Error) {
LOG_ERR("SCT", "Parse error during incremental build");
abandonBuild();
return false;
}
if (status == ChapterHtmlSlimParser::ParseStatus::Done) {
return finalizeBuild();
}
// ParseStatus::More: yield once we've laid out the requested number of pages.
if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) {
build_->bytesConsumed = build_->parser->parseBytesConsumed();
return true;
}
}
}
bool Section::hasHtmlCache() const {
const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html";
return Storage.exists(htmlPath.c_str());
}
std::optional<uint16_t> Section::findAnchorDuringBuild(const std::string& anchor) const {
if (!build_ || !build_->parser) return std::nullopt;
for (const auto& [key, page] : build_->parser->getAnchors()) {
if (key == anchor) return page;
}
return std::nullopt;
}
std::optional<uint16_t> Section::findAnchor(const std::string& anchor) const {
if (const auto page = findAnchorDuringBuild(anchor)) {
return page;
}
// Fall back to the on-disk anchor map: a finalized section, or a partial whose map
// covers everything up to its watermark (nullopt past it -- build further and retry).
return getPageForAnchor(anchor);
}
uint16_t Section::estimatedTotalPages() const {
// Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA
// damping is needed. Also the best guess while a rebuild is running but hasn't laid out
// enough pages yet to extrapolate from its own progress.
const auto partialEstimate = [this]() -> uint16_t {
if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) {
return pageCount;
}
const uint64_t est = static_cast<uint64_t>(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_;
if (est <= pageCount) return pageCount;
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
};
if (!build_) {
return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact
}
const uint32_t consumed = build_->bytesConsumed;
const uint32_t total = build_->totalBytes;
if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate();
// Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This
// re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses
// dense vs sparse regions of the chapter.
const uint64_t raw = static_cast<uint64_t>(builtPageCount_) * total / consumed;
// Damp that jitter with an exponential moving average. Step it once per build advance (keyed on
// bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how
// often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so
// the average settles onto the true count (and finalizeBuild then returns the exact pageCount).
constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle
if (build_->smoothedEstimate <= 0) {
build_->smoothedEstimate = static_cast<float>(raw); // seed on the first estimate
} else if (consumed != build_->smoothedAtConsumed) {
build_->smoothedEstimate += ALPHA * (static_cast<float>(raw) - build_->smoothedEstimate);
}
build_->smoothedAtConsumed = consumed;
const uint64_t est = static_cast<uint64_t>(build_->smoothedEstimate + 0.5f);
if (est <= pageCount) return pageCount; // never fewer than the pages already available
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
}
// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built
// page count and table offsets, stamp `version` as the commit point, then swap the tmp
// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer
// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate
// the total page count. The parser must still be alive (anchors are read from it).
// On failure the tmp is removed and any pre-existing file at filePath is left intact.
bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) {
const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION);
const auto failCommit = [this]() {
// Explicit close() required before remove (member variable, O_RDWR handle).
file.close();
Storage.remove(binTmpPath().c_str());
return false;
};
const uint32_t lutOffset = file.position(); const uint32_t lutOffset = file.position();
bool hasFailedLutRecords = false; for (const auto& entry : build_->lut) {
// Write LUT
for (const auto& entry : lut) {
if (entry.fileOffset == 0) { if (entry.fileOffset == 0) {
hasFailedLutRecords = true; LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
break; return failCommit();
} }
serialization::writePod(file, entry.fileOffset); serialization::writePod(file, entry.fileOffset);
} }
if (hasFailedLutRecords) { // Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions"); // partial, skip anchors that landed on the incomplete trailing page the suspend drops.
// Explicitly close() file before calling Storage.remove()
file.close();
Storage.remove(filePath.c_str());
return false;
}
// Write anchor-to-page map for fragment navigation (e.g. footnote targets)
const uint32_t anchorMapOffset = file.position(); const uint32_t anchorMapOffset = file.position();
const auto& anchors = visitor.getAnchors(); const auto& anchors = build_->parser->getAnchors();
serialization::writePod(file, static_cast<uint16_t>(anchors.size())); uint16_t anchorCount = 0;
for (const auto& [anchor, page] : anchors) { for (const auto& [anchor, page] : anchors) {
if (!asPartial || page < builtPageCount_) anchorCount++;
}
serialization::writePod(file, anchorCount);
for (const auto& [anchor, page] : anchors) {
if (asPartial && page >= builtPageCount_) continue;
serialization::writeString(file, anchor); serialization::writeString(file, anchor);
serialization::writePod(file, page); serialization::writePod(file, page);
} }
const uint32_t paragraphLutOffset = file.position(); const uint32_t paragraphLutOffset = file.position();
serialization::writePod(file, static_cast<uint16_t>(lut.size())); serialization::writePod(file, static_cast<uint16_t>(build_->lut.size()));
for (const auto& entry : lut) { for (const auto& entry : build_->lut) {
serialization::writePod(file, entry.paragraphIndex); serialization::writePod(file, entry.paragraphIndex);
} }
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position()); const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
for (const auto& entry : lut) { for (const auto& entry : build_->lut) {
serialization::writePod(file, entry.listItemIndex); serialization::writePod(file, entry.listItemIndex);
} }
// Patch header with final pageCount, lutOffset, anchorMapOffset, paragraphLutOffset, and liLutOffset if (asPartial) {
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(pageCount)); // Watermark trailer, located on load as liLutOffset + pageCount * sizeof(uint16_t).
serialization::writePod(file, pageCount); serialization::writePod(file, bytesConsumed);
serialization::writePod(file, totalBytes);
}
// Patch header with the built page count and section offsets...
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(builtPageCount_));
serialization::writePod(file, builtPageCount_);
serialization::writePod(file, lutOffset); serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset); serialization::writePod(file, anchorMapOffset);
serialization::writePod(file, paragraphLutOffset); serialization::writePod(file, paragraphLutOffset);
serialization::writePod(file, liLutFileOffset); serialization::writePod(file, liLutFileOffset);
// ...then commit by overwriting the sentinel version with the real one. Writing the
// version last makes it the commit point: a crash before here leaves version 0.
file.seek(0);
serialization::writePod(file, version);
// Explicit close() required: member variable persists beyond function scope // Explicit close() required: member variable persists beyond function scope
file.close(); file.close();
if (cssParser) {
cssParser->clear(); // Swap into place. A crash between remove and rename loses the old file but keeps a
// fully-committed tmp; the next build just removes it and rebuilds.
if (Storage.exists(filePath.c_str())) {
Storage.remove(filePath.c_str());
}
if (!Storage.rename(binTmpPath().c_str(), filePath.c_str())) {
LOG_ERR("SCT", "Failed to move built section into place");
Storage.remove(binTmpPath().c_str());
return false;
} }
return true; return true;
} }
std::unique_ptr<Page> Section::loadPageFromSectionFile() { bool Section::finalizeBuild() {
if (!Storage.openFileForRead("SCT", filePath, file)) { // Flush the trailing page (emits the last page via the completePageFn into the LUT).
build_->parser->finishParse();
if (!build_->reusedHtml) {
// Parse succeeded: promote the freshly unzipped HTML to the persistent cache so future
// rebuilds skip zip inflation. If promotion fails, drop the temp -- the build still succeeded.
if (!Storage.rename(build_->tmpHtmlPath.c_str(), build_->htmlPath.c_str())) {
LOG_DBG("SCT", "Failed to promote HTML cache, removing temp");
Storage.remove(build_->tmpHtmlPath.c_str());
}
}
const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0);
if (build_->cssParser) build_->cssParser->clear();
build_.reset();
if (!committed) {
// commitBuildFile removed filePath before the failed swap, so nothing valid remains.
partial_ = false;
partialPageCount_ = 0;
pageCount = 0;
builtPageCount_ = 0;
return false;
}
buildComplete_ = true;
partial_ = false;
partialPageCount_ = 0;
pageCount = builtPageCount_;
return true;
}
void Section::suspendBuild() {
if (!build_) return;
// Only worth persisting if this build produced pages a pre-existing partial doesn't
// already cover; otherwise keep the older (bigger) partial and just drop the tmp.
const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_);
bool committed = false;
if (worthKeeping) {
// Capture the parse watermark and commit before tearing the parser down (the anchor
// map is read from it). The incomplete trailing page is intentionally not flushed:
// only fully laid-out pages are persisted, and the rebuild re-derives the rest.
const uint32_t consumed = static_cast<uint32_t>(build_->parser->parseBytesConsumed());
committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes);
if (committed) {
partial_ = true;
partialPageCount_ = builtPageCount_;
partialBytesConsumed_ = consumed;
partialTotalBytes_ = build_->totalBytes;
LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_);
}
}
if (build_->parser) build_->parser->abortParse();
if (build_->cssParser) build_->cssParser->clear();
if (!committed && file) {
// Explicit close() required before remove (member variable, O_RDWR handle).
file.close();
Storage.remove(binTmpPath().c_str());
}
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
Storage.remove(build_->tmpHtmlPath.c_str());
}
build_.reset();
buildComplete_ = false;
pageCount = partial_ ? partialPageCount_ : 0;
builtPageCount_ = 0;
}
void Section::abandonBuild() {
if (!build_) return;
if (build_->parser) build_->parser->abortParse();
if (build_->cssParser) build_->cssParser->clear();
if (file) {
// Explicit close() required before remove (member variable, O_RDWR handle).
file.close();
Storage.remove(binTmpPath().c_str());
}
// A parse error would recur against the same HTML, so drop any partial too -- resuming
// from it would just re-enter the failing build every open.
if (Storage.exists(filePath.c_str())) {
Storage.remove(filePath.c_str());
}
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
Storage.remove(build_->tmpHtmlPath.c_str());
}
build_.reset();
buildComplete_ = false;
partial_ = false;
partialPageCount_ = 0;
pageCount = 0;
builtPageCount_ = 0;
}
std::unique_ptr<Page> Section::loadPageDuringBuild(const int page) {
if (!build_ || page < 0 || page >= static_cast<int>(build_->lut.size()) || !file) {
return nullptr;
}
const uint32_t pos = build_->lut[page].fileOffset;
if (pos == 0) {
return nullptr;
}
// The .bin is open O_RDWR for the build. Read the already-written page, then restore
// the write cursor so the next onPageComplete keeps appending where it left off.
const uint32_t writePos = file.position();
file.seek(pos);
auto p = Page::deserialize(file);
file.seek(writePos);
return p;
}
// Read a page from the committed file at filePath (finalized section or partial from a
// previous session). Uses a local handle so it is safe while a build holds the member
// `file` open on the tmp .bin.
std::unique_ptr<Page> Section::loadPageAt(const int page) const {
HalFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return nullptr; return nullptr;
} }
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4); f.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
uint32_t lutOffset; uint32_t lutOffset;
serialization::readPod(file, lutOffset); serialization::readPod(f, lutOffset);
file.seek(lutOffset + sizeof(uint32_t) * currentPage); f.seek(lutOffset + sizeof(uint32_t) * page);
uint32_t pagePos; uint32_t pagePos;
serialization::readPod(file, pagePos); serialization::readPod(f, pagePos);
file.seek(pagePos); f.seek(pagePos);
auto page = Page::deserialize(file); return Page::deserialize(f);
// Explicit close() required: member variable persists beyond function scope // No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
file.close(); }
return page;
std::unique_ptr<Page> Section::loadPage(const int page) {
if (page < 0) {
return nullptr;
}
if (build_ && page < static_cast<int>(build_->lut.size())) {
return loadPageDuringBuild(page);
}
// Not (yet) in the active build: serve from the file on disk -- a finalized section,
// or a partial from a previous session whose pages the rebuild hasn't reached again.
const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount);
if (page >= onDisk) {
return nullptr;
}
return loadPageAt(page);
} }
std::string Section::getTextFromSectionFile() { std::string Section::getTextFromSectionFile() {
std::string fullText; std::string fullText;
auto p = this->loadPageFromSectionFile(); auto p = loadPage(currentPage);
if (p) { if (p) {
for (const auto& el : p->elements) { for (const auto& el : p->elements) {
if (el->getTag() == TAG_PageLine) { if (el->getTag() == TAG_PageLine) {
@@ -360,6 +756,15 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
return std::nullopt; return std::nullopt;
} }
// Only a finalized section's count is the chapter total; a partial's count is just the
// suspended build's watermark, which would skew progress mapping. Callers fall back to
// their own estimates.
uint8_t version;
serialization::readPod(f, version);
if (version != SECTION_FILE_VERSION) {
return std::nullopt;
}
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t)); f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
uint16_t count; uint16_t count;
serialization::readPod(f, count); serialization::readPod(f, count);
+105 -7
View File
@@ -3,11 +3,14 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
#include "Epub.h" #include "Epub.h"
class Page; class Page;
class GfxRenderer; class GfxRenderer;
class ChapterHtmlSlimParser;
class CssParser;
class Section { class Section {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
@@ -21,16 +24,67 @@ class Section {
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled); bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
uint32_t onPageComplete(std::unique_ptr<Page> page); uint32_t onPageComplete(std::unique_ptr<Page> page);
// Page-offset table entry, kept in RAM while an incremental build is running so
// already-built pages can be located in the partially-written .bin.
struct PageLutEntry {
uint32_t fileOffset;
uint16_t paragraphIndex;
uint16_t listItemIndex;
};
// Held only while an incremental build is in progress (see startBuild). Carries the
// live parser plus the strings it references (the parser stores them by reference)
// and the in-RAM page-offset table.
struct BuildContext {
std::unique_ptr<ChapterHtmlSlimParser> parser;
std::vector<PageLutEntry> lut;
std::string parsePath;
std::string contentBase;
std::string imageBasePath;
std::string htmlPath;
std::string tmpHtmlPath;
bool reusedHtml = false;
CssParser* cssParser = nullptr;
// HTML byte progress, for estimating the section's total page count while it's still building.
uint32_t bytesConsumed = 0;
uint32_t totalBytes = 0;
// Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its
// last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions;
// the EMA is stepped once per build advance (not per redraw) to damp that wobble.
float smoothedEstimate = 0;
uint32_t smoothedAtConsumed = 0;
};
std::unique_ptr<BuildContext> build_;
bool buildComplete_ = false;
// Pages laid out by the active build (== build_->lut.size()). Distinct from pageCount,
// which is the pages *available to read* and also counts a loaded partial file's pages.
uint16_t builtPageCount_ = 0;
// A partial section file (suspended build from a previous session) is loaded at filePath.
// Its pages 0..partialPageCount_-1 are readable while a rebuild extends past them.
bool partial_ = false;
uint16_t partialPageCount_ = 0;
// Parse watermark from the partial's trailer, for estimating the total page count.
uint32_t partialBytesConsumed_ = 0;
uint32_t partialTotalBytes_ = 0;
bool finalizeBuild();
// Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the
// header, stamp the version byte, and swap the tmp .bin over filePath.
bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes);
// 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.
std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const;
// 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.
std::unique_ptr<Page> loadPageDuringBuild(int page);
public: public:
uint16_t pageCount = 0; uint16_t pageCount = 0;
int currentPage = 0; int currentPage = 0;
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer) // Constructor and destructor are out-of-line: BuildContext holds a unique_ptr to the
: epub(epub), // forward-declared ChapterHtmlSlimParser, whose full definition is only visible in the .cpp.
spineIndex(spineIndex), explicit Section(const std::shared_ptr<Epub>& epub, int spineIndex, GfxRenderer& renderer);
renderer(renderer), ~Section();
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, bool focusReadingEnabled); uint8_t imageRendering, bool focusReadingEnabled);
@@ -39,12 +93,56 @@ class Section {
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, bool focusReadingEnabled, uint8_t imageRendering, bool focusReadingEnabled,
const std::function<void()>& popupFn = nullptr); const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Incremental build: lay out the section a few pages at a time so a large chapter
// can show its first page immediately and keep the UI responsive while the rest
// builds. createSectionFile() above is the one-shot wrapper over these.
// if (!startBuild(...)) fail;
// each tick: buildSomeMore(N); render up to pageCount; when isBuildComplete() stop.
bool startBuild(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, bool focusReadingEnabled, const std::function<void()>& popupFn = nullptr);
// Lay out up to maxPages more pages (maxPages <= 0 = build to completion). Returns
// false on error (the build is abandoned). Sets isBuildComplete() when finished.
bool buildSomeMore(int maxPages);
bool isBuilding() const { return static_cast<bool>(build_); }
bool isBuildComplete() const { return buildComplete_; }
// Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based
// estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine
// is still building, so "page X of Y" / progress don't read off the small build watermark.
uint16_t estimatedTotalPages() const;
void abandonBuild();
// Persist an in-progress build as a partial section file (version sentinel + LUTs +
// watermark trailer) instead of discarding it, so the next open of this spine can show
// its pages instantly and only rebuild in the background. Called by the destructor, so
// any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a
// pre-existing partial when it covers more pages than this build reached.
void suspendBuild();
// True when a partial file was loaded: pageCount is a watermark, not the chapter total.
bool isPartial() const { return partial_; }
// Unified page read: from the active build if it has reached the page, otherwise from
// the on-disk file (finalized section, or a partial the rebuild hasn't caught up to).
std::unique_ptr<Page> loadPage(int page);
std::string getTextFromSectionFile(); std::string getTextFromSectionFile();
// Resolve an anchor from the in-progress build first, then the on-disk anchor map
// (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// 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.
bool hasHtmlCache() const;
// Look up the page number for an anchor id from the section cache file. // Look up the page number for an anchor id from the section cache file.
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const; std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
// Look up an anchor among the pages built so far by the in-progress build, so an anchor jump
// (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the
// whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build.
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
// Get the page count from the section cache file without fully loading it. // Get the page count from the section cache file without fully loading it.
std::optional<uint16_t> getCachedPageCount() const; std::optional<uint16_t> getCachedPageCount() const;
+71 -5
View File
@@ -21,6 +21,39 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
const bool scanning = renderer.isFontCacheScanning(); const bool scanning = renderer.isFontCacheScanning();
const int ascender = renderer.getFontAscenderSize(fontId); const int ascender = renderer.getFontAscenderSize(fontId);
struct DecorationLineTracker {
EpdFontFamily::Style style;
int yOffset;
int startX = -1;
int endX = -1;
int yPos = 0;
bool active() const { return startX != -1; }
void reset() {
startX = -1;
endX = -1;
yPos = 0;
}
};
DecorationLineTracker decorationLines[] = {
{EpdFontFamily::UNDERLINE, ascender + 2},
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
};
const auto flushDecoration = [&](DecorationLineTracker& line) {
if (line.active()) {
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
line.reset();
}
};
const auto flushDecorations = [&]() {
for (auto& line : decorationLines) {
flushDecoration(line);
}
};
for (size_t i = 0; i < words.size(); i++) { for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x; const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i]; const EpdFontFamily::Style currentStyle = wordStyles[i];
@@ -59,18 +92,51 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir); renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir);
} }
if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) { if (scanning) {
continue;
}
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
const std::string& w = words[i]; const std::string& w = words[i];
int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir); int lineStartX = wordX;
const int underlineY = wordY + ascender + 2; int lineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) { if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
underlineWidth = (underlineWidth + 1) / 2; lineWidth = (lineWidth + 1) / 2;
} }
renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true); // Do not decorate the synthetic em-space used for paragraph indentation.
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
static_cast<uint8_t>(w[2]) == 0x83) {
const char* visibleText = w.c_str() + 3;
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
lineWidth = (lineWidth + 1) / 2;
}
}
for (auto& line : decorationLines) {
if ((currentStyle & line.style) == 0) {
flushDecoration(line);
continue;
}
const int lineY = wordY + line.yOffset;
if (line.active() && line.yPos != lineY) {
flushDecoration(line);
}
if (!line.active()) {
line.startX = lineStartX;
line.yPos = lineY;
}
line.endX = lineStartX + lineWidth;
}
} else {
flushDecorations();
} }
} }
flushDecorations();
} }
bool TextBlock::serialize(HalFile& file) const { bool TextBlock::serialize(HalFile& file) const {
+15 -13
View File
@@ -67,13 +67,6 @@ constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKe
[](char a, char b) { return asciiToLower(a) == b; }); [](char a, char b) { return asciiToLower(a) == b; });
} }
// Case-insensitive ASCII substring search. Only needed by text-decoration,
// which accepts multi-value strings like "underline solid red".
constexpr bool icontainsAscii(std::string_view value, std::string_view lowercaseKeyword) {
return std::search(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
[](char a, char b) { return asciiToLower(a) == b; }) != value.end();
}
// Walk s and invoke fn(token) for each non-empty run between delimiters. // Walk s and invoke fn(token) for each non-empty run between delimiters.
// Tokens are boundary-trimmed and yielded as string_views into s; no // Tokens are boundary-trimmed and yielded as string_views into s; no
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are // allocation. Runs of consecutive delimiters coalesce — no empty tokens are
@@ -252,11 +245,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
} }
CssTextDecoration CssParser::interpretDecoration(std::string_view val) { CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
// text-decoration can have multiple space-separated values // text-decoration can have multiple space-separated values. Compare whole tokens
if (icontainsAscii(val, "underline")) { // so malformed values like "notunderline" do not accidentally enable a line.
return CssTextDecoration::Underline; CssTextDecoration result = CssTextDecoration::None;
} bool explicitNone = false;
return CssTextDecoration::None; forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
if (iequalsAscii(token, "none")) {
explicitNone = true;
} else if (iequalsAscii(token, "underline")) {
result = result | CssTextDecoration::Underline;
} else if (iequalsAscii(token, "line-through")) {
result = result | CssTextDecoration::LineThrough;
}
});
return explicitNone ? CssTextDecoration::None : result;
} }
CssLength CssParser::interpretLength(std::string_view val) { CssLength CssParser::interpretLength(std::string_view val) {
@@ -868,7 +870,7 @@ bool CssParser::loadFromCache() {
rulesBySelector_.clear(); rulesBySelector_.clear();
return false; return false;
} }
style.textDecoration = static_cast<CssTextDecoration>(enumVal); style.textDecoration = static_cast<CssTextDecoration>(enumVal & CSS_TEXT_DECORATION_MASK);
if (file.read(&enumVal, 1) != 1) { if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear(); rulesBySelector_.clear();
+1 -1
View File
@@ -33,7 +33,7 @@
class CssParser { class CssParser {
public: public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes // Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 6; static constexpr uint8_t CSS_CACHE_VERSION = 7;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default; ~CssParser() = default;
+13 -2
View File
@@ -52,8 +52,19 @@ enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
// Font weight options - CSS supports 100-900, we simplify to normal/bold // Font weight options - CSS supports 100-900, we simplify to normal/bold
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 }; enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
// Text decoration options // Text decoration options. Values are bit flags so CSS can combine multiple line decorations.
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 }; enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
}
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
}
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
// Display options - only None and Block are relevant for e-ink rendering // Display options - only None and Block are relevant for e-ink rendering
enum class CssDisplay : uint8_t { Block = 0, None = 1 }; enum class CssDisplay : uint8_t { Block = 0, None = 1 };
+51 -48
View File
@@ -13,54 +13,57 @@ struct EntityPair {
// Sorted lexicographically by key to allow binary search. // Sorted lexicographically by key to allow binary search.
static constexpr EntityPair ENTITY_LOOKUP[] = { static constexpr EntityPair ENTITY_LOOKUP[] = {
{"&AElig;", "Æ"}, {"&Aacute;", "Á"}, {"&Acirc;", "Â"}, {"&Agrave;", "À"}, {"&Alpha;", "Α"}, {"&AElig;", "Æ"}, {"&Aacute;", "Á"}, {"&Acirc;", "Â"}, {"&Agrave;", "À"}, {"&Alpha;", "Α"},
{"&Aring;", "Å"}, {"&Atilde;", "Ã"}, {"&Auml;", "Ä"}, {"&Beta;", "Β"}, {"&Ccedil;", "Ç"}, {"&Aring;", "Å"}, {"&Atilde;", "Ã"}, {"&Auml;", "Ä"}, {"&Beta;", "Β"}, {"&Ccedil;", "Ç"},
{"&Chi;", "Χ"}, {"&Dagger;", ""}, {"&Delta;", "Δ"}, {"&ETH;", "Ð"}, {"&Eacute;", "É"}, {"&Chi;", "Χ"}, {"&Dagger;", ""}, {"&Delta;", "Δ"}, {"&ETH;", "Ð"}, {"&Eacute;", "É"},
{"&Ecirc;", "Ê"}, {"&Egrave;", "È"}, {"&Epsilon;", "Ε"}, {"&Eta;", "Η"}, {"&Euml;", "Ë"}, {"&Ecirc;", "Ê"}, {"&Egrave;", "È"}, {"&Epsilon;", "Ε"}, {"&Eta;", "Η"}, {"&Euml;", "Ë"},
{"&Gamma;", "Γ"}, {"&Iacute;", "Í"}, {"&Icirc;", "Î"}, {"&Igrave;", "Ì"}, {"&Iota;", "Ι"}, {"&Gamma;", "Γ"}, {"&Iacute;", "Í"}, {"&Icirc;", "Î"}, {"&Igrave;", "Ì"}, {"&Iota;", "Ι"},
{"&Iuml;", "Ï"}, {"&Kappa;", "Κ"}, {"&Lambda;", "Λ"}, {"&Mu;", "Μ"}, {"&Ntilde;", "Ñ"}, {"&Iuml;", "Ï"}, {"&Kappa;", "Κ"}, {"&Lambda;", "Λ"}, {"&Mu;", "Μ"}, {"&Ntilde;", "Ñ"},
{"&Nu;", "Ν"}, {"&OElig;", "Œ"}, {"&Oacute;", "Ó"}, {"&Ocirc;", "Ô"}, {"&Ograve;", "Ò"}, {"&Nu;", "Ν"}, {"&OElig;", "Œ"}, {"&Oacute;", "Ó"}, {"&Ocirc;", "Ô"}, {"&Ograve;", "Ò"},
{"&Omega;", "Ω"}, {"&Omicron;", "Ο"}, {"&Oslash;", "Ø"}, {"&Otilde;", "Õ"}, {"&Ouml;", "Ö"}, {"&Omega;", "Ω"}, {"&Omicron;", "Ο"}, {"&Oslash;", "Ø"}, {"&Otilde;", "Õ"}, {"&Ouml;", "Ö"},
{"&Phi;", "Φ"}, {"&Pi;", "Π"}, {"&Prime;", ""}, {"&Psi;", "Ψ"}, {"&Rho;", "Ρ"}, {"&Phi;", "Φ"}, {"&Pi;", "Π"}, {"&Prime;", ""}, {"&Psi;", "Ψ"}, {"&Rho;", "Ρ"},
{"&Scaron;", "Š"}, {"&Sigma;", "Σ"}, {"&THORN;", "Þ"}, {"&Tau;", "Τ"}, {"&Theta;", "Θ"}, {"&Scaron;", "Š"}, {"&Sigma;", "Σ"}, {"&THORN;", "Þ"}, {"&Tau;", "Τ"}, {"&Theta;", "Θ"},
{"&Uacute;", "Ú"}, {"&Ucirc;", "Û"}, {"&Ugrave;", "Ù"}, {"&Upsilon;", "Υ"}, {"&Uuml;", "Ü"}, {"&Uacute;", "Ú"}, {"&Ucirc;", "Û"}, {"&Ugrave;", "Ù"}, {"&Upsilon;", "Υ"}, {"&Uuml;", "Ü"},
{"&Xi;", "Ξ"}, {"&Yacute;", "Ý"}, {"&Yuml;", "Ÿ"}, {"&Zeta;", "Ζ"}, {"&aacute;", "á"}, {"&Xi;", "Ξ"}, {"&Yacute;", "Ý"}, {"&Yuml;", "Ÿ"}, {"&Zeta;", "Ζ"}, {"&aacute;", "á"},
{"&acirc;", "â"}, {"&acute;", "´"}, {"&aelig;", "æ"}, {"&agrave;", "à"}, {"&alpha;", "α"}, {"&acirc;", "â"}, {"&acute;", "´"}, {"&aelig;", "æ"}, {"&agrave;", "à"}, {"&alefsym;", ""},
{"&amp;", "&"}, {"&and;", ""}, {"&ang;", ""}, {"&aring;", "å"}, {"&asymp;", ""}, {"&alpha;", "α"}, {"&amp;", "&"}, {"&and;", ""}, {"&ang;", ""}, {"&aring;", "å"},
{"&atilde;", "ã"}, {"&auml;", "ä"}, {"&bdquo;", ""}, {"&beta;", "β"}, {"&brvbar;", "¦"}, {"&asymp;", ""}, {"&atilde;", "ã"}, {"&auml;", "ä"}, {"&bdquo;", ""}, {"&beta;", "β"},
{"&bull;", ""}, {"&cap;", ""}, {"&ccedil;", "ç"}, {"&cedil;", "¸"}, {"&cent;", "¢"}, {"&brvbar;", "¦"}, {"&bull;", ""}, {"&cap;", ""}, {"&ccedil;", "ç"}, {"&cedil;", "¸"},
{"&chi;", "χ"}, {"&circ;", "ˆ"}, {"&clubs;", ""}, {"&cong;", ""}, {"&copy;", "©"}, {"&cent;", "¢"}, {"&chi;", "χ"}, {"&circ;", "ˆ"}, {"&clubs;", ""}, {"&cong;", ""},
{"&crarr;", ""}, {"&cup;", ""}, {"&curren;", "¤"}, {"&dagger;", ""}, {"&darr;", ""}, {"&copy;", "©"}, {"&crarr;", ""}, {"&cup;", ""}, {"&curren;", "¤"}, {"&dArr;", ""},
{"&deg;", "°"}, {"&delta;", "δ"}, {"&diams;", ""}, {"&divide;", "÷"}, {"&eacute;", "é"}, {"&dagger;", ""}, {"&darr;", ""}, {"&deg;", "°"}, {"&delta;", "δ"}, {"&diams;", ""},
{"&ecirc;", "ê"}, {"&egrave;", "è"}, {"&empty;", ""}, {"&emsp;", " "}, {"&ensp;", " "}, {"&divide;", "÷"}, {"&eacute;", "é"}, {"&ecirc;", "ê"}, {"&egrave;", "è"}, {"&empty;", ""},
{"&epsilon;", "ε"}, {"&equiv;", ""}, {"&eta;", "η"}, {"&eth;", "ð"}, {"&euml;", "ë"}, {"&emsp;", " "}, {"&ensp;", " "}, {"&epsilon;", "ε"}, {"&equiv;", ""}, {"&eta;", "η"},
{"&euro;", ""}, {"&exist;", ""}, {"&fnof;", "ƒ"}, {"&forall;", ""}, {"&frac12;", "½"}, {"&eth;", "ð"}, {"&euml;", "ë"}, {"&euro;", ""}, {"&exist;", ""}, {"&fnof;", "ƒ"},
{"&frac14;", "¼"}, {"&frac34;", "¾"}, {"&frasl;", ""}, {"&gamma;", "γ"}, {"&ge;", ""}, {"&forall;", ""}, {"&frac12;", "½"}, {"&frac14;", "¼"}, {"&frac34;", "¾"}, {"&frasl;", ""},
{"&gt;", ">"}, {"&harr;", ""}, {"&hearts;", ""}, {"&hellip;", ""}, {"&iacute;", "í"}, {"&gamma;", "γ"}, {"&ge;", ""}, {"&gt;", ">"}, {"&hArr;", ""}, {"&harr;", ""},
{"&icirc;", "î"}, {"&iexcl;", "¡"}, {"&igrave;", "ì"}, {"&infin;", ""}, {"&int;", ""}, {"&hearts;", ""}, {"&hellip;", ""}, {"&iacute;", "í"}, {"&icirc;", "î"}, {"&iexcl;", "¡"},
{"&iota;", "ι"}, {"&iquest;", "¿"}, {"&isin;", ""}, {"&iuml;", "ï"}, {"&kappa;", "κ"}, {"&igrave;", "ì"}, {"&image;", ""}, {"&infin;", ""}, {"&int;", ""}, {"&iota;", "ι"},
{"&lambda;", "λ"}, {"&laquo;", "«"}, {"&larr;", ""}, {"&lceil;", ""}, {"&ldquo;", "\u201C"}, {"&iquest;", "¿"}, {"&isin;", ""}, {"&iuml;", "ï"}, {"&kappa;", "κ"}, {"&lArr;", ""},
{"&le;", ""}, {"&lfloor;", ""}, {"&lowast;", ""}, {"&loz;", ""}, {"&lrm;", "\u200E"}, {"&lambda;", "λ"}, {"&lang;", ""}, {"&laquo;", "«"}, {"&larr;", ""}, {"&lceil;", ""},
{"&lsaquo;", ""}, {"&lsquo;", "\u2018"}, {"&lt;", "<"}, {"&macr;", "¯"}, {"&mdash;", ""}, {"&ldquo;", "\u201C"}, {"&le;", ""}, {"&lfloor;", ""}, {"&lowast;", ""}, {"&loz;", ""},
{"&micro;", "µ"}, {"&minus;", ""}, {"&mu;", "μ"}, {"&nabla;", ""}, {"&nbsp;", "\xC2\xA0"}, {"&lrm;", "\u200E"}, {"&lsaquo;", ""}, {"&lsquo;", "\u2018"}, {"&lt;", "<"}, {"&macr;", "¯"},
{"&ndash;", ""}, {"&ne;", ""}, {"&ni;", ""}, {"&not;", "¬"}, {"&notin;", ""}, {"&mdash;", ""}, {"&micro;", "µ"}, {"&middot;", "·"}, {"&minus;", ""}, {"&mu;", "μ"},
{"&nsub;", ""}, {"&ntilde;", "ñ"}, {"&nu;", "ν"}, {"&oacute;", "ó"}, {"&ocirc;", "ô"}, {"&nabla;", ""}, {"&nbsp;", "\xC2\xA0"}, {"&ndash;", ""}, {"&ne;", ""}, {"&ni;", ""},
{"&oelig;", "œ"}, {"&ograve;", "ò"}, {"&oline;", ""}, {"&omega;", "ω"}, {"&omicron;", "ο"}, {"&not;", "¬"}, {"&notin;", ""}, {"&nsub;", ""}, {"&ntilde;", "ñ"}, {"&nu;", "ν"},
{"&oplus;", ""}, {"&or;", ""}, {"&ordf;", "ª"}, {"&ordm;", "º"}, {"&oslash;", "ø"}, {"&oacute;", "ó"}, {"&ocirc;", "ô"}, {"&oelig;", "œ"}, {"&ograve;", "ò"}, {"&oline;", ""},
{"&otilde;", "õ"}, {"&otimes;", ""}, {"&ouml;", "ö"}, {"&para;", ""}, {"&part;", ""}, {"&omega;", "ω"}, {"&omicron;", "ο"}, {"&oplus;", ""}, {"&or;", ""}, {"&ordf;", "ª"},
{"&permil;", ""}, {"&perp;", ""}, {"&phi;", "φ"}, {"&pi;", "π"}, {"&piv;", "ϖ"}, {"&ordm;", "º"}, {"&oslash;", "ø"}, {"&otilde;", "õ"}, {"&otimes;", ""}, {"&ouml;", "ö"},
{"&plusmn;", "±"}, {"&pound;", "£"}, {"&prime;", ""}, {"&prod;", ""}, {"&prop;", ""}, {"&para;", ""}, {"&part;", ""}, {"&permil;", ""}, {"&perp;", ""}, {"&phi;", "φ"},
{"&psi;", "ψ"}, {"&quot;", "\""}, {"&radic;", ""}, {"&raquo;", "»"}, {"&rarr;", ""}, {"&pi;", "π"}, {"&piv;", "ϖ"}, {"&plusmn;", "±"}, {"&pound;", "£"}, {"&prime;", ""},
{"&rceil;", ""}, {"&rdquo;", "\u201D"}, {"&reg;", "®"}, {"&rfloor;", ""}, {"&rho;", "ρ"}, {"&prod;", ""}, {"&prop;", ""}, {"&psi;", "ψ"}, {"&quot;", "\""}, {"&rArr;", ""},
{"&rlm;", "\u200F"}, {"&rsaquo;", ""}, {"&rsquo;", "\u2019"}, {"&sbquo;", ""}, {"&scaron;", "š"}, {"&radic;", ""}, {"&rang;", ""}, {"&raquo;", "»"}, {"&rarr;", ""}, {"&rceil;", ""},
{"&sdot;", ""}, {"&sect;", "§"}, {"&shy;", "\xC2\xAD"}, {"&sigma;", "σ"}, {"&sigmaf;", "ς"}, {"&rdquo;", "\u201D"}, {"&real;", "\u211C"}, {"&reg;", "®"}, {"&rfloor;", ""}, {"&rho;", "ρ"},
{"&sim;", ""}, {"&spades;", ""}, {"&sub;", ""}, {"&sube;", ""}, {"&sum;", ""}, {"&rlm;", "\u200F"}, {"&rsaquo;", ""}, {"&rsquo;", "\u2019"}, {"&sbquo;", ""}, {"&scaron;", "š"},
{"&sup1;", "¹"}, {"&sup2;", "²"}, {"&sup3;", "³"}, {"&sup;", ""}, {"&supe;", ""}, {"&sdot;", ""}, {"&sect;", "§"}, {"&shy;", "\xC2\xAD"}, {"&sigma;", "σ"}, {"&sigmaf;", "ς"},
{"&szlig;", "ß"}, {"&tau;", "τ"}, {"&there4;", ""}, {"&theta;", "θ"}, {"&thetasym;", "ϑ"}, {"&sim;", ""}, {"&spades;", ""}, {"&sub;", ""}, {"&sube;", ""}, {"&sum;", ""},
{"&thinsp;", " "}, {"&thorn;", "þ"}, {"&tilde;", "˜"}, {"&times;", "×"}, {"&trade;", ""}, {"&sup1;", "¹"}, {"&sup2;", "²"}, {"&sup3;", "³"}, {"&sup;", ""}, {"&supe;", ""},
{"&uacute;", "ú"}, {"&uarr;", ""}, {"&ucirc;", "û"}, {"&ugrave;", "ù"}, {"&uml;", "¨"}, {"&szlig;", "ß"}, {"&tau;", "τ"}, {"&there4;", ""}, {"&theta;", "θ"}, {"&thetasym;", "ϑ"},
{"&upsih;", "ϒ"}, {"&upsilon;", "υ"}, {"&uuml;", "ü"}, {"&xi;", "ξ"}, {"&yacute;", "ý"}, {"&thinsp;", " "}, {"&thorn;", "þ"}, {"&tilde;", "˜"}, {"&times;", "×"}, {"&trade;", ""},
{"&yen;", "¥"}, {"&yuml;", "ÿ"}, {"&zeta;", "ζ"}, {"&zwj;", "\u200D"}, {"&zwnj;", "\u200C"}, {"&uArr;", ""}, {"&uacute;", "ú"}, {"&uarr;", ""}, {"&ucirc;", "û"}, {"&ugrave;", "ù"},
{"&uml;", "¨"}, {"&upsih;", "ϒ"}, {"&upsilon;", "υ"}, {"&uuml;", "ü"}, {"&weierp;", ""},
{"&xi;", "ξ"}, {"&yacute;", "ý"}, {"&yen;", "¥"}, {"&yuml;", "ÿ"}, {"&zeta;", "ζ"},
{"&zwj;", "\u200D"}, {"&zwnj;", "\u200C"},
}; };
// Verify the table is sorted at compile time. // Verify the table is sorted at compile time.
+145 -93
View File
@@ -34,6 +34,7 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr const char* BOLD_TAGS[] = {"b", "strong"}; 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* IMAGE_TAGS[] = {"img"}; constexpr const char* IMAGE_TAGS[] = {"img"};
constexpr const char* SKIP_TAGS[] = {"head"}; constexpr const char* SKIP_TAGS[] = {"head"};
@@ -89,13 +90,50 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const
} }
} }
// Update effective bold/italic/underline based on block style and inline style stack EpdFontFamily::Style ChapterHtmlSlimParser::fontStyleForTextDecoration(const CssTextDecoration decoration) {
EpdFontFamily::Style style = EpdFontFamily::REGULAR;
if ((decoration & CssTextDecoration::Underline) != CssTextDecoration::None) {
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::UNDERLINE);
}
if ((decoration & CssTextDecoration::LineThrough) != CssTextDecoration::None) {
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::STRIKETHROUGH);
}
return style;
}
void ChapterHtmlSlimParser::applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css) {
if (css.hasTextDecoration()) {
entry.hasTextDecoration = true;
entry.textDecoration = css.textDecoration;
}
}
void ChapterHtmlSlimParser::pushDecorationStyleEntry(const CssTextDecoration defaultDecoration,
const CssStyle& cssStyle) {
StyleStackEntry entry;
entry.depth = depth;
entry.hasTextDecoration = true;
entry.textDecoration = cssStyle.hasTextDecoration() ? cssStyle.textDecoration : defaultDecoration;
if (cssStyle.hasFontWeight()) {
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
}
if (cssStyle.hasFontStyle()) {
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
applyDirectionToEntry(entry, cssStyle);
inlineStyleStack.push_back(entry);
updateEffectiveInlineStyle();
}
// Update effective bold/italic/decorations based on block style and inline style stack
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
// Start with block-level styles // Start with block-level styles
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold; effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic; effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
effectiveUnderline = effectiveTextDecoration =
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline; currentCssStyle.hasTextDecoration() ? currentCssStyle.textDecoration : CssTextDecoration::None;
effectiveDirectionDefined = currentCssStyle.hasDirection(); effectiveDirectionDefined = currentCssStyle.hasDirection();
effectiveDirection = currentCssStyle.direction; effectiveDirection = currentCssStyle.direction;
effectiveSup = false; effectiveSup = false;
@@ -109,8 +147,10 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
if (entry.hasItalic) { if (entry.hasItalic) {
effectiveItalic = entry.italic; effectiveItalic = entry.italic;
} }
if (entry.hasUnderline) { // CSS line decorations propagate through descendants; child entries add
effectiveUnderline = entry.underline; // their own lines but cannot cancel an ancestor's already active line.
if (entry.hasTextDecoration) {
effectiveTextDecoration = effectiveTextDecoration | entry.textDecoration;
} }
if (entry.hasDirection) { if (entry.hasDirection) {
effectiveDirectionDefined = true; effectiveDirectionDefined = true;
@@ -164,7 +204,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
// Determine font style from depth-based tracking and CSS effective style // Determine font style from depth-based tracking and CSS effective style
const bool isBold = boldUntilDepth < depth || effectiveBold; const bool isBold = boldUntilDepth < depth || effectiveBold;
const bool isItalic = italicUntilDepth < depth || effectiveItalic; const bool isItalic = italicUntilDepth < depth || effectiveItalic;
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
// Combine style flags using bitwise OR // Combine style flags using bitwise OR
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
@@ -174,9 +213,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
if (isItalic) { if (isItalic) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC); fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
} }
if (isUnderline) { fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | fontStyleForTextDecoration(effectiveTextDecoration));
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
}
if (effectiveSup) { if (effectiveSup) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP); fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
} else if (effectiveSub) { } else if (effectiveSub) {
@@ -317,6 +354,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
const bool isTocAnchor = const bool isTocAnchor =
std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end(); std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end();
if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) { if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) {
// Flush a displaced anchor before overwriting. Consecutive non-block elements
// (e.g. <aside id="fn1">text</aside><aside id="fn2">) with no intervening block
// never trigger startNewTextBlock, so fn1 gets silently overwritten. That leaves
// fn1 missing from the anchor map -> getPageForAnchor returns nullopt -> reader
// lands at page 0 (section start) instead of the footnote.
if (!self->pendingAnchorId.empty()) {
self->flushPendingAnchor();
}
self->pendingAnchorId = idValue; self->pendingAnchorId = idValue;
} }
} else if (strcmp(atts[i], "dir") == 0) { } else if (strcmp(atts[i], "dir") == 0) {
@@ -412,14 +457,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
headerStyle.bold = false; headerStyle.bold = false;
headerStyle.hasItalic = true; headerStyle.hasItalic = true;
headerStyle.italic = true; headerStyle.italic = true;
headerStyle.hasUnderline = true;
headerStyle.underline = false;
self->inlineStyleStack.push_back(headerStyle); self->inlineStyleStack.push_back(headerStyle);
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
const CssTextDecoration savedTextDecoration = self->effectiveTextDecoration;
self->effectiveTextDecoration = CssTextDecoration::None;
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length())); self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
if (self->partWordBufferIndex > 0) { if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer(); self->flushPartWordBuffer();
} }
self->effectiveTextDecoration = savedTextDecoration;
self->nextWordContinues = false; self->nextWordContinues = false;
self->inlineStyleStack.pop_back(); self->inlineStyleStack.pop_back();
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
@@ -752,12 +798,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->currentFootnote.number[0] = '\0'; self->currentFootnote.number[0] = '\0';
self->currentFootnoteLinkTextLen = 0; self->currentFootnoteLinkTextLen = 0;
// Apply underline style to visually indicate the link // Apply underline style to visually indicate the link.
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
StyleStackEntry entry; StyleStackEntry entry;
entry.depth = self->depth; entry.depth = self->depth;
entry.hasUnderline = true; entry.hasTextDecoration = true;
entry.underline = true; entry.textDecoration = CssTextDecoration::Underline;
applyDirectionToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry); self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
@@ -829,23 +874,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->flushPartWordBuffer(); self->flushPartWordBuffer();
self->nextWordContinues = true; self->nextWordContinues = true;
} }
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth); self->pushDecorationStyleEntry(CssTextDecoration::Underline, cssStyle);
// Push inline style entry for underline tag } else if (matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS))) {
StyleStackEntry entry; // Flush buffer before style change so preceding text gets current style
entry.depth = self->depth; // Track depth for matching pop if (self->partWordBufferIndex > 0) {
entry.hasUnderline = true; self->flushPartWordBuffer();
entry.underline = true; self->nextWordContinues = true;
if (cssStyle.hasFontWeight()) {
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
} }
if (cssStyle.hasFontStyle()) { self->pushDecorationStyleEntry(CssTextDecoration::LineThrough, cssStyle);
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) { } else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
// Flush buffer before style change so preceding text gets current style // Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) { if (self->partWordBufferIndex > 0) {
@@ -862,10 +898,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasItalic = true; entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
} }
if (cssStyle.hasTextDecoration()) { applyTextDecorationToEntry(entry, cssStyle);
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyDirectionToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry); self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
@@ -885,10 +918,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasBold = true; entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold; entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
} }
if (cssStyle.hasTextDecoration()) { applyTextDecorationToEntry(entry, cssStyle);
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyDirectionToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry); self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
@@ -927,10 +957,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasItalic = true; entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
} }
if (cssStyle.hasTextDecoration()) { applyTextDecorationToEntry(entry, cssStyle);
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyDirectionToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle);
if (cssStyle.hasVerticalAlign()) { if (cssStyle.hasVerticalAlign()) {
if (cssStyle.verticalAlign == CssVerticalAlign::Super) { if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
@@ -1142,9 +1169,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1; !self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
const bool willClearBold = self->boldUntilDepth == self->depth - 1; const bool willClearBold = self->boldUntilDepth == self->depth - 1;
const bool willClearItalic = self->italicUntilDepth == self->depth - 1; const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline; const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic;
const bool headerOrBlockTag = isHeaderOrBlock(name); const bool headerOrBlockTag = isHeaderOrBlock(name);
const bool tableStructuralTag = isTableStructuralTag(name); const bool tableStructuralTag = isTableStructuralTag(name);
@@ -1163,7 +1189,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1; !matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) || const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) || matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag || matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) ||
matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS)) || tableStructuralTag ||
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1; matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
if (shouldFlush) { if (shouldFlush) {
@@ -1222,11 +1249,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->italicUntilDepth = INT_MAX; self->italicUntilDepth = INT_MAX;
} }
// Leaving underline tag
if (self->underlineUntilDepth == self->depth) {
self->underlineUntilDepth = INT_MAX;
}
// Pop from inline style stack if we pushed an entry at this depth // Pop from inline style stack if we pushed an entry at this depth
// This handles all inline elements: b, i, u, span, etc. // This handles all inline elements: b, i, u, span, etc.
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) { if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
@@ -1253,7 +1275,9 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
} }
} }
bool ChapterHtmlSlimParser::parseAndBuildPages() { ChapterHtmlSlimParser::~ChapterHtmlSlimParser() { abortParse(); }
bool ChapterHtmlSlimParser::beginParse() {
// Initialize block style stack with a root entry representing "no ancestor block elements". // Initialize block style stack with a root entry representing "no ancestor block elements".
// The user's paragraph alignment is set as the default so child elements without explicit // The user's paragraph alignment is set as the default so child elements without explicit
// text-align inherit it correctly through getCombinedBlockStyle. // text-align inherit it correctly through getCombinedBlockStyle.
@@ -1271,67 +1295,78 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
paragraphAlignmentBlockStyle.alignment = align; paragraphAlignmentBlockStyle.alignment = align;
startNewTextBlock(paragraphAlignmentBlockStyle); startNewTextBlock(paragraphAlignmentBlockStyle);
XML_Parser parser = XML_ParserCreate(nullptr); xmlParser_ = XML_ParserCreate(nullptr);
int done; if (!xmlParser_) {
if (!parser) {
LOG_ERR("EHP", "Couldn't allocate memory for parser"); LOG_ERR("EHP", "Couldn't allocate memory for parser");
return false; return false;
} }
// Handle HTML entities (like &nbsp;) that aren't in XML spec or DTD // Handle HTML entities (like &nbsp;) that aren't in XML spec or DTD
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE // Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand); XML_SetDefaultHandlerExpand(xmlParser_, defaultHandlerExpand);
HalFile file; if (!Storage.openFileForRead("EHP", filepath, parseFile_)) {
if (!Storage.openFileForRead("EHP", filepath, file)) { destroyXmlParser(xmlParser_);
destroyXmlParser(parser); xmlParser_ = nullptr;
return false; return false;
} }
// Get file size to decide whether to show indexing popup. // Get file size to decide whether to show indexing popup.
if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) { if (popupFn && parseFile_.size() >= MIN_SIZE_FOR_POPUP) {
popupFn(); popupFn();
} }
XML_SetUserData(parser, this); XML_SetUserData(xmlParser_, this);
XML_SetElementHandler(parser, startElement, endElement); XML_SetElementHandler(xmlParser_, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData); XML_SetCharacterDataHandler(xmlParser_, characterData);
// Compute the time taken to parse and build pages parseStartTime_ = millis();
const uint32_t chapterStartTime = millis(); return true;
do { }
void* const buf = XML_GetBuffer(parser, PARSE_BUFFER_SIZE);
if (!buf) {
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
destroyXmlParser(parser);
file.close();
return false;
}
const size_t len = file.read(buf, PARSE_BUFFER_SIZE); ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() {
void* const buf = XML_GetBuffer(xmlParser_, PARSE_BUFFER_SIZE);
if (!buf) {
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
return ParseStatus::Error;
}
if (len == 0 && file.available() > 0) { const size_t len = parseFile_.read(buf, PARSE_BUFFER_SIZE);
LOG_ERR("EHP", "File read error");
destroyXmlParser(parser);
file.close();
return false;
}
done = file.available() == 0; if (len == 0 && parseFile_.available() > 0) {
LOG_ERR("EHP", "File read error");
return ParseStatus::Error;
}
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) { const int done = parseFile_.available() == 0;
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
destroyXmlParser(parser);
file.close();
return false;
}
} while (!done);
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - chapterStartTime);
destroyXmlParser(parser); if (XML_ParseBuffer(xmlParser_, static_cast<int>(len), done) == XML_STATUS_ERROR) {
file.close(); LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(xmlParser_),
XML_ErrorString(XML_GetErrorCode(xmlParser_)));
return ParseStatus::Error;
}
return done ? ParseStatus::Done : ParseStatus::More;
}
void ChapterHtmlSlimParser::abortParse() {
if (xmlParser_) {
destroyXmlParser(xmlParser_);
xmlParser_ = nullptr;
}
// Only close the file if it was successfully opened in beginParse()
if (parseFile_.isOpen()) {
parseFile_.close();
}
}
bool ChapterHtmlSlimParser::finishParse() {
if (xmlParser_) {
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - parseStartTime_);
destroyXmlParser(xmlParser_);
xmlParser_ = nullptr;
}
parseFile_.close();
// Process last page if there is still text // Process last page if there is still text
if (currentTextBlock) { if (currentTextBlock) {
@@ -1349,6 +1384,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return true; return true;
} }
bool ChapterHtmlSlimParser::parseAndBuildPages() {
if (!beginParse()) {
return false;
}
for (;;) {
const ParseStatus status = parseStep();
if (status == ParseStatus::Error) {
abortParse();
return false;
}
if (status == ParseStatus::Done) {
break;
}
}
return finishParse();
}
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) { void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression; const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
+38 -4
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <HalStorage.h>
#include <expat.h> #include <expat.h>
#include <climits> #include <climits>
@@ -31,7 +32,6 @@ class ChapterHtmlSlimParser {
int skipUntilDepth = INT_MAX; int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX; int italicUntilDepth = INT_MAX;
int underlineUntilDepth = INT_MAX;
// buffer for building up words from characters, will auto break if longer than this // buffer for building up words from characters, will auto break if longer than this
// leave one char at end for null pointer // leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {}; char partWordBuffer[MAX_WORD_SIZE + 1] = {};
@@ -60,7 +60,8 @@ class ChapterHtmlSlimParser {
int depth = 0; int depth = 0;
bool hasBold = false, bold = false; bool hasBold = false, bold = false;
bool hasItalic = false, italic = false; bool hasItalic = false, italic = false;
bool hasUnderline = false, underline = false; bool hasTextDecoration = false;
CssTextDecoration textDecoration = CssTextDecoration::None;
bool hasDirection = false; bool hasDirection = false;
CssTextDirection direction = CssTextDirection::Ltr; CssTextDirection direction = CssTextDirection::Ltr;
bool hasSup = false, sup = false; bool hasSup = false, sup = false;
@@ -71,7 +72,7 @@ class ChapterHtmlSlimParser {
CssStyle currentCssStyle; CssStyle currentCssStyle;
bool effectiveBold = false; bool effectiveBold = false;
bool effectiveItalic = false; bool effectiveItalic = false;
bool effectiveUnderline = false; CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
bool effectiveDirectionDefined = false; bool effectiveDirectionDefined = false;
CssTextDirection effectiveDirection = CssTextDirection::Ltr; CssTextDirection effectiveDirection = CssTextDirection::Ltr;
bool effectiveSup = false; bool effectiveSup = false;
@@ -96,12 +97,25 @@ class ChapterHtmlSlimParser {
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry> std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
int wordsExtractedInBlock = 0; int wordsExtractedInBlock = 0;
// Resumable parse state. The one-shot parseAndBuildPages() drives these
// internally; the incremental section builder drives them across render ticks
// so a large single chapter can yield between pages instead of blocking the UI
// until the whole thing is laid out. parseFile_ and the expat parser stay alive
// for the lifetime of the parse so it can be paused and resumed at buffer
// boundaries.
XML_Parser xmlParser_ = nullptr;
HalFile parseFile_;
uint32_t parseStartTime_ = 0;
void updateEffectiveInlineStyle(); void updateEffectiveInlineStyle();
void startNewTextBlock(const BlockStyle& blockStyle); void startNewTextBlock(const BlockStyle& blockStyle);
void flushPendingAnchor(); void flushPendingAnchor();
void flushPartWordBuffer(); void flushPartWordBuffer();
void makePages(); void makePages();
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css); static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
void emitHorizontalRule(const BlockStyle& blockStyle); void emitHorizontalRule(const BlockStyle& blockStyle);
// XML callbacks // XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
@@ -141,8 +155,28 @@ class ChapterHtmlSlimParser {
imageBasePath(imageBasePath), imageBasePath(imageBasePath),
tocAnchors(std::move(tocAnchors)) {} tocAnchors(std::move(tocAnchors)) {}
~ChapterHtmlSlimParser() = default; ~ChapterHtmlSlimParser();
// One-shot parse: builds every page before returning (begin + step* + finish).
bool parseAndBuildPages(); bool parseAndBuildPages();
// Resumable parse, for the incremental section builder. Drive as:
// if (!beginParse()) fail;
// loop: switch (parseStep()) { More: keep going / yield; Done: finishParse(); Error: abortParse(); }
// Pages are emitted via completePageFn as they complete during parseStep(), so
// the caller can stop once enough pages are built and resume on a later tick.
enum class ParseStatus { More, Done, Error };
bool beginParse();
ParseStatus parseStep();
bool finishParse(); // flush the trailing page and tear down; returns true
void abortParse(); // tear down without flushing (error / abandon)
void addLineToPage(std::shared_ptr<TextBlock> line); void addLineToPage(std::shared_ptr<TextBlock> line);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; } const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
// between beginParse() and finishParse()/abortParse().
size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : 0; }
size_t parseTotalBytes() { return parseFile_ ? parseFile_.size() : 0; }
}; };
+70 -54
View File
@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <string_view>
#include <vector> #include <vector>
namespace FsHelpers { namespace FsHelpers {
@@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) {
} }
std::string normalisePath(const std::string& path) { std::string normalisePath(const std::string& path) {
std::vector<std::string> components; std::vector<std::string_view> components;
std::string component; components.reserve(8); // Eight nested folders is more than we might expect
for (const auto c : path) { size_t start = 0;
if (c == '/') { for (size_t i = 0; i <= path.length(); ++i) {
if (!component.empty()) { if (i == path.length() || path[i] == '/') {
if (i > start) {
std::string_view component(path.data() + start, i - start);
if (component == "..") { if (component == "..") {
if (!components.empty()) { if (!components.empty()) {
components.pop_back(); components.pop_back();
@@ -49,28 +52,80 @@ std::string normalisePath(const std::string& path) {
} else { } else {
components.push_back(component); components.push_back(component);
} }
component.clear();
} }
} else { start = i + 1;
component += c;
} }
} }
if (!component.empty()) { if (components.empty()) {
components.push_back(component); return "";
}
size_t total_len = 0;
for (const auto& c : components) {
total_len += c.length() + 1;
} }
std::string result; std::string result;
for (const auto& c : components) { result.reserve(total_len - 1);
if (!result.empty()) {
result += "/"; for (size_t i = 0; i < components.size(); ++i) {
if (i > 0) {
result += '/';
} }
result += c; result.append(components[i].data(), components[i].length());
} }
return result; return result;
} }
bool naturalLess(const std::string& str1, const std::string& str2) {
// Naive natural sort: numeric-aware, case-insensitive
const char* s1 = str1.c_str();
const char* s2 = str2.c_str();
// ctype functions require unsigned char values: passing a negative char (UTF-8
// bytes above 0x7f with signed char) is undefined behavior
const auto isDigit = [](const char c) { return isdigit(static_cast<unsigned char>(c)) != 0; };
// Iterate while both strings have characters
while (*s1 && *s2) {
// Check if both are at the start of a number
if (isDigit(*s1) && isDigit(*s2)) {
// Skip leading zeros and track them
while (*s1 == '0') s1++;
while (*s2 == '0') s2++;
// Count digits to compare lengths first
int len1 = 0, len2 = 0;
while (isDigit(s1[len1])) len1++;
while (isDigit(s2[len2])) len2++;
// Different length so return smaller integer value
if (len1 != len2) return len1 < len2;
// Same length so compare digit by digit
for (int i = 0; i < len1; i++) {
if (s1[i] != s2[i]) return s1[i] < s2[i];
}
// Numbers equal so advance pointers
s1 += len1;
s2 += len2;
} else {
// Regular case-insensitive character comparison
const int c1 = tolower(static_cast<unsigned char>(*s1));
const int c2 = tolower(static_cast<unsigned char>(*s2));
if (c1 != c2) return c1 < c2;
s1++;
s2++;
}
}
// One string is prefix of other
return *s1 == '\0' && *s2 != '\0';
}
void sortFileList(std::vector<std::string>& strs) { void sortFileList(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
// Directories first // Directories first
@@ -78,46 +133,7 @@ void sortFileList(std::vector<std::string>& strs) {
bool isDir2 = str2.back() == '/'; bool isDir2 = str2.back() == '/';
if (isDir1 != isDir2) return isDir1; if (isDir1 != isDir2) return isDir1;
// Start naive natural sort return naturalLess(str1, str2);
const char* s1 = str1.c_str();
const char* s2 = str2.c_str();
// Iterate while both strings have characters
while (*s1 && *s2) {
// Check if both are at the start of a number
if (isdigit(*s1) && isdigit(*s2)) {
// Skip leading zeros and track them
while (*s1 == '0') s1++;
while (*s2 == '0') s2++;
// Count digits to compare lengths first
int len1 = 0, len2 = 0;
while (isdigit(s1[len1])) len1++;
while (isdigit(s2[len2])) len2++;
// Different length so return smaller integer value
if (len1 != len2) return len1 < len2;
// Same length so compare digit by digit
for (int i = 0; i < len1; i++) {
if (s1[i] != s2[i]) return s1[i] < s2[i];
}
// Numbers equal so advance pointers
s1 += len1;
s2 += len2;
} else {
// Regular case-insensitive character comparison
char c1 = tolower(*s1);
char c2 = tolower(*s2);
if (c1 != c2) return c1 < c2;
s1++;
s2++;
}
}
// One string is prefix of other
return *s1 == '\0' && *s2 != '\0';
}); });
} }
+4
View File
@@ -11,6 +11,10 @@ std::string decodeUriEscapes(const std::string& path);
std::string normalisePath(const std::string& path); std::string normalisePath(const std::string& path);
// Numeric-aware, case-insensitive comparison ("2" < "10"). Returns true when str1 orders
// before str2. Same ordering sortFileList applies within the file/directory groups.
bool naturalLess(const std::string& str1, const std::string& str2);
void sortFileList(std::vector<std::string>& strs); void sortFileList(std::vector<std::string>& strs);
/** /**
+248 -19
View File
@@ -282,8 +282,17 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3); const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3);
if (renderMode == GfxRenderer::BW && bmpVal < 3) { if (renderMode == GfxRenderer::BW && bmpVal < 3) {
// Black (also paints over the grays in BW mode) // Default: gray pixels paint solid black. Required when grayscale
renderer.drawPixel(screenX, screenY, pixelState); // passes follow (their LUT lightens pixels it assumes were just
// driven solid black), and it is the plain non-AA look otherwise.
// Fast AA: partial (anti-aliased edge) coverage dithers with the
// same period-2 patterns as fillRectDither (dark grey = 50%
// checkerboard, light grey = 25%), approximating AA in a single
// BW pass. Skipped pixels leave the background untouched.
if (!renderer.isFastAntiAliasing() || bmpVal == 0 || (bmpVal == 1 && ((screenX + screenY) & 1) == 0) ||
(bmpVal == 2 && (screenX & 1) == 0 && (screenY & 1) == 0)) {
renderer.drawPixel(screenX, screenY, pixelState);
}
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
// Light gray (also mark the MSB if it's going to be a dark gray too) // Light gray (also mark the MSB if it's going to be a dark gray too)
// Dedicated X3 gray LUTs now provide proper 4-level gray on both devices // Dedicated X3 gray LUTs now provide proper 4-level gray on both devices
@@ -661,8 +670,10 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con
} }
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
for (int fillY = y; fillY < y + height; fillY++) { if (state) {
drawLine(x, fillY, x + width - 1, fillY, state); fillRectImpl<Color::Black>(x, y, width, height);
} else {
fillRectImpl<Color::White>(x, y, width, height);
} }
} }
@@ -694,26 +705,194 @@ void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) con
} }
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const { void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
if (color == Color::Clear) { switch (color) {
} else if (color == Color::Black) { case Color::Clear:
fillRect(x, y, width, height, true); break;
} else if (color == Color::White) { case Color::Black:
fillRect(x, y, width, height, false); fillRectImpl<Color::Black>(x, y, width, height);
} else if (color == Color::LightGray) { break;
for (int fillY = y; fillY < y + height; fillY++) { case Color::White:
for (int fillX = x; fillX < x + width; fillX++) { fillRectImpl<Color::White>(x, y, width, height);
drawPixelDither<Color::LightGray>(fillX, fillY); break;
case Color::LightGray:
fillRectImpl<Color::LightGray>(x, y, width, height);
break;
case Color::DarkGray:
fillRectImpl<Color::DarkGray>(x, y, width, height);
break;
}
}
template <Color C>
void GfxRenderer::fillRectImpl(const int x, const int y, const int width, const int height) const {
if constexpr (C == Color::Clear) return;
if (width <= 0 || height <= 0) return;
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
// Clip in logical space.
const int screenW = getScreenWidth();
const int screenH = getScreenHeight();
const int lx0 = std::max(0, x);
const int ly0 = std::max(0, y);
const int lx1 = std::min(screenW, x + width);
const int ly1 = std::min(screenH, y + height);
if (lx0 >= lx1 || ly0 >= ly1) return;
// Rotate the two opposing logical corners into physical-framebuffer space.
// The bounding rect in physical space is the rect we need to fill — rotation
// is rigid (no shear/stretch) so the bbox of the two corners IS the rect.
int paX, paY, pbX, pbY;
rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight);
rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight);
const int phyX0 = std::min(paX, pbX);
const int phyX1 = std::max(paX, pbX); // inclusive
int phyY0 = std::min(paY, pbY);
int phyY1 = std::max(paY, pbY);
// Strip mode: clip Y range to the active band and redirect writes.
uint8_t* target = getWriteTarget();
const int originY = getWriteOriginY();
const int writeRows = getWriteRows();
phyY0 = std::max(phyY0, originY);
phyY1 = std::min(phyY1, originY + writeRows - 1);
if (phyY0 > phyY1) return;
// Bit/byte layout: MSB-first within a byte, so phyX → bit (7 - (phyX & 7)).
// Head and tail masks cover only the in-rect bits of the first/last byte.
const int byteStart = phyX0 >> 3;
const int byteEnd = phyX1 >> 3; // inclusive
const uint8_t headMask = static_cast<uint8_t>(0xFFu >> (phyX0 & 7));
const uint8_t tailMask = static_cast<uint8_t>(0xFFu << (7 - (phyX1 & 7)));
const int32_t panelStride = static_cast<int32_t>(panelWidthBytes);
if constexpr (C == Color::Black || C == Color::White) {
// Solid fill. Framebuffer: 0 = black, 1 = white.
const uint8_t fillByte = (C == Color::Black) ? 0x00u : 0xFFu;
for (int py = phyY0; py <= phyY1; ++py) {
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
if (byteStart == byteEnd) {
const uint8_t mask = headMask & tailMask;
if constexpr (C == Color::Black) {
row[byteStart] &= static_cast<uint8_t>(~mask);
} else {
row[byteStart] |= mask;
}
} else {
if constexpr (C == Color::Black) {
row[byteStart] &= static_cast<uint8_t>(~headMask);
if (byteEnd > byteStart + 1) {
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
}
row[byteEnd] &= static_cast<uint8_t>(~tailMask);
} else {
row[byteStart] |= headMask;
if (byteEnd > byteStart + 1) {
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
}
row[byteEnd] |= tailMask;
}
} }
} }
} else if (color == Color::DarkGray) { } else {
for (int fillY = y; fillY < y + height; fillY++) { // Dither (LightGray / DarkGray). Both patterns have period 2 in logical
for (int fillX = x; fillX < x + width; fillX++) { // (x, y), so per physical row we precompute one byte that represents the
drawPixelDither<Color::DarkGray>(fillX, fillY); // pattern across an 8-pixel stretch — every full byte in the row uses
// that same value.
//
// dlxPerPhyX / dlyPerPhyX: how logical (x, y) change as phyX increments
// along a physical row. Derived from inverting rotateCoordinates.
int dlxPerPhyX = 0, dlyPerPhyX = 0;
switch (orientation) {
case Portrait:
dlxPerPhyX = 0;
dlyPerPhyX = 1;
break;
case PortraitInverted:
dlxPerPhyX = 0;
dlyPerPhyX = -1;
break;
case LandscapeClockwise:
dlxPerPhyX = -1;
dlyPerPhyX = 0;
break;
case LandscapeCounterClockwise:
dlxPerPhyX = 1;
dlyPerPhyX = 0;
break;
}
// The dither pattern has period 2 in logical space, and each orientation
// maps py to logical coords with a fixed parity relationship. The
// blackMask byte therefore repeats with period 2 in py. Precompute both
// variants outside the row loop to eliminate the per-row switch + 8-bit
// construction loop.
uint8_t blackMasks[2];
for (int parityIdx = 0; parityIdx < 2; ++parityIdx) {
const int samplePy = phyY0 + parityIdx;
int lxBase = 0, lyBase = 0;
switch (orientation) {
case Portrait:
lxBase = panelHeight - 1 - samplePy;
lyBase = byteStart * 8;
break;
case PortraitInverted:
lxBase = samplePy;
lyBase = panelWidth - 1 - byteStart * 8;
break;
case LandscapeClockwise:
lxBase = panelWidth - 1 - byteStart * 8;
lyBase = panelHeight - 1 - samplePy;
break;
case LandscapeCounterClockwise:
lxBase = byteStart * 8;
lyBase = samplePy;
break;
}
uint8_t mask = 0;
for (int b = 0; b < 8; ++b) {
const int lx = lxBase + b * dlxPerPhyX;
const int ly = lyBase + b * dlyPerPhyX;
bool isBlack;
if constexpr (C == Color::LightGray) {
isBlack = ((lx & 1) == 0) && ((ly & 1) == 0);
} else { // DarkGray
isBlack = (((lx + ly) & 1) == 0);
}
if (isBlack) mask |= static_cast<uint8_t>(1u << (7 - b));
}
blackMasks[samplePy & 1] = mask;
}
for (int py = phyY0; py <= phyY1; ++py) {
const uint8_t blackMask = blackMasks[py & 1];
const uint8_t whiteMask = static_cast<uint8_t>(~blackMask);
// Dither writes BOTH inks (the slow path called drawPixel for every
// pixel — setting or clearing — so we must do the same). Inside the
// rect mask: write whiteMask (1s where white, 0s where black). Outside
// the rect mask: leave the framebuffer untouched.
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
if (byteStart == byteEnd) {
const uint8_t rectMask = headMask & tailMask;
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~rectMask) | (rectMask & whiteMask));
} else {
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~headMask) | (headMask & whiteMask));
if (byteEnd > byteStart + 1) {
// Period 2, so every full byte in this row is exactly whiteMask.
memset(row + byteStart + 1, whiteMask, byteEnd - byteStart - 1);
}
row[byteEnd] = static_cast<uint8_t>((row[byteEnd] & ~tailMask) | (tailMask & whiteMask));
} }
} }
} }
} }
template void GfxRenderer::fillRectImpl<Color::Black>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::White>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::LightGray>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::DarkGray>(int, int, int, int) const;
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height, void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
const int radius, const Color color) const { const int radius, const Color color) const {
if (radius <= 0 || color == Color::Clear) { if (radius <= 0 || color == Color::Clear) {
@@ -884,8 +1063,24 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
display.drawImage(bitmap, rotatedX, rotatedY, width, height); display.drawImage(bitmap, rotatedX, rotatedY, width, height);
} }
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int size) const {
display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width); // Plot the icon pixel-by-pixel through drawPixel (which applies the orientation
// transform) instead of the byte-aligned framebuffer blit. The blit snaps the
// icon's position to 8px (one byte) along the rotated axis, which prevents it
// from aligning with adjacent text; per-pixel plotting is pixel-precise.
// Icons are square and 1bpp (MSB-first, bit==0 = ink). The (size-1-row, col)
// mapping reproduces the Portrait orientation the blit produced; drawIcon is
// only called by the UI themes, which all render in forced Portrait.
const int rowBytes = (size + 7) / 8;
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
const uint8_t byte = bitmap[row * rowBytes + (col >> 3)];
const bool ink = ((byte >> (7 - (col & 7))) & 1) == 0;
if (ink) {
drawPixel(x + (size - 1 - row), y + col, true);
}
}
}
} }
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
@@ -1437,8 +1632,18 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
int32_t widthFP = 0; int32_t widthFP = 0;
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0; const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style); const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
}
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))) {
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx); int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
const EpdGlyph* glyph = font.getGlyph(cp, style);
advFP = glyph ? glyph->advanceX : 0;
}
widthFP += isSupSub ? (advFP + 1) / 2 : advFP; widthFP += isSupSub ? (advFP + 1) / 2 : advFP;
} }
return fp4::toPixel(widthFP); return fp4::toPixel(widthFP);
@@ -1577,6 +1782,30 @@ size_t GfxRenderer::getBufferSize() const { return frameBufferSize; }
// unused // unused
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } // void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
void GfxRenderer::displayGrayscaleBase(HalDisplay::RefreshMode fallback) const {
display.displayGrayscaleBase(fallback, fadingFix);
}
void GfxRenderer::preconditionGrayscale() const { display.preconditionGrayscale(); }
void GfxRenderer::preconditionGrayscale(int x, int y, int w, int h) const {
if (w <= 0 || h <= 0) return;
// Rotate the logical rect's opposite corners to physical panel coords; the
// physical bbox stays axis-aligned for all four orientations.
int ax, ay, bx, by;
rotateCoordinates(orientation, x, y, &ax, &ay, panelWidth, panelHeight);
rotateCoordinates(orientation, x + w - 1, y + h - 1, &bx, &by, panelWidth, panelHeight);
int x0 = ax < bx ? ax : bx, x1 = ax > bx ? ax : bx;
int y0 = ay < by ? ay : by, y1 = ay > by ? ay : by;
if (x0 < 0) x0 = 0;
if (y0 < 0) y0 = 0;
if (x1 >= panelWidth) x1 = panelWidth - 1;
if (y1 >= panelHeight) y1 = panelHeight - 1;
if (x1 < x0 || y1 < y0) return;
display.preconditionGrayscale(static_cast<uint16_t>(x0), static_cast<uint16_t>(y0),
static_cast<uint16_t>(x1 - x0 + 1), static_cast<uint16_t>(y1 - y0 + 1));
}
void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); } void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); }
void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); } void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); }
+26 -1
View File
@@ -44,6 +44,13 @@ class GfxRenderer {
RenderMode renderMode; RenderMode renderMode;
Orientation orientation; Orientation orientation;
bool fadingFix; bool fadingFix;
// Fast text anti-aliasing: when set, BW-mode rendering of 2-bit glyphs
// dithers partial-coverage (gray) pixels instead of painting them solid
// black, approximating AA in a single BW pass with no grayscale refresh.
// Must stay false when grayscale LSB/MSB passes follow the BW render: the
// gray LUT lightens pixels it assumes were just driven solid black, and
// dither-skipped white pixels would take its white-drive frames unopposed.
bool fastAntiAliasing = false;
uint8_t* frameBuffer = nullptr; uint8_t* frameBuffer = nullptr;
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH; uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT; uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
@@ -81,6 +88,12 @@ class GfxRenderer {
void drawPixelDither(int x, int y) const; void drawPixelDither(int x, int y) const;
template <Color color> template <Color color>
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
// Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's
// two opposing corners into physical-framebuffer space once, then walks each
// physical row with head-mask / middle memset / tail-mask byte writes — no
// per-pixel rotation, no per-pixel RMW.
template <Color color>
void fillRectImpl(int x, int y, int width, int height) const;
public: public:
explicit GfxRenderer(HalDisplay& halDisplay) explicit GfxRenderer(HalDisplay& halDisplay)
@@ -177,7 +190,7 @@ class GfxRenderer {
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight, void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
bool roundBottomLeft, bool roundBottomRight, Color color) const; bool roundBottomLeft, bool roundBottomRight, Color color) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const; void drawIcon(const uint8_t bitmap[], int x, int y, int size) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
float cropY = 0) const; float cropY = 0) const;
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const; void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
@@ -218,6 +231,18 @@ class GfxRenderer {
// Grayscale functions // Grayscale functions
void setRenderMode(const RenderMode mode) { this->renderMode = mode; } void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
RenderMode getRenderMode() const { return renderMode; } RenderMode getRenderMode() const { return renderMode; }
void setFastAntiAliasing(const bool enabled) { this->fastAntiAliasing = enabled; }
bool isFastAntiAliasing() const { return fastAntiAliasing; }
// Grayscale preconditioning settle pass (no-op on X4). The rect overload
// takes the gray region in LOGICAL screen coordinates and rotates it to the
// panel; the no-arg overload settles the full frame. Call after the BW base
// frame is displayed and before the grayscale planes are written.
void preconditionGrayscale() const;
void preconditionGrayscale(int x, int y, int w, int h) const;
// Display the framebuffer as the base frame for a grayscale overlay that
// follows (X3: OEM differential base waveform; others: plain display with
// `fallback`).
void displayGrayscaleBase(HalDisplay::RefreshMode fallback = HalDisplay::HALF_REFRESH) const;
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer() const;
+9 -2
View File
@@ -65,11 +65,14 @@ STR_SLEEP_COVER_MODE: "Рэжым вокладкі сну"
STR_HIDE_BATTERY: "Схаваць % батарэі" STR_HIDE_BATTERY: "Схаваць % батарэі"
STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца" STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца"
STR_TEXT_AA: "Згладжванне тэксту" STR_TEXT_AA: "Згладжванне тэксту"
STR_TEXT_AA_FULL: "Поўнае"
STR_TEXT_AA_FAST: "Хуткае"
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR" STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
STR_ORIENTATION: "Арыентацыя чытання" STR_ORIENTATION: "Арыентацыя чытання"
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі" STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела" STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
STR_FONT_FAMILY: "Шрыфт чытання" STR_FONT_FAMILY: "Шрыфт чытання"
STR_FONT_SIZE: "Памер шрыфту інтэрфейсу" STR_FONT_SIZE: "Памер шрыфту інтэрфейсу"
STR_LINE_SPACING: "Міжрадковы інтэрвал" STR_LINE_SPACING: "Міжрадковы інтэрвал"
@@ -125,6 +128,7 @@ STR_PAGE_TURN: "Перагортванне"
STR_PORTRAIT: "Партрэт" STR_PORTRAIT: "Партрэт"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Інверсія" STR_INVERTED: "Інверсія"
STR_ORIENTATION_INVERTED: "Партрэт 180°"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Наперад" STR_PREV_NEXT: "Назад/Наперад"
STR_NEXT_PREV: "Наперад/Назад" STR_NEXT_PREV: "Наперад/Назад"
@@ -175,6 +179,9 @@ STR_EXIT: "« Выхад"
STR_HOME: "« Галоўная" STR_HOME: "« Галоўная"
STR_SELECT: "Абраць" STR_SELECT: "Абраць"
STR_TOGGLE: "Выбар" STR_TOGGLE: "Выбар"
STR_TOGGLE_BOOKMARK: "Пераключыць закладку"
STR_BOOKMARK_REMOVED: "Закладка выдалена."
STR_HOLD_OPEN_TO_DELETE: "Утрымлівайце Адкрыць, каб выдаліць"
STR_CONFIRM: "Пацв." STR_CONFIRM: "Пацв."
STR_CANCEL: "Адмена" STR_CANCEL: "Адмена"
STR_CONNECT: "Падкл." STR_CONNECT: "Падкл."
@@ -231,7 +238,6 @@ STR_CHAPTER_PREFIX: "Раздзел:"
STR_PAGES_SEPARATOR: "стар. |" STR_PAGES_SEPARATOR: "стар. |"
STR_BOOK_PREFIX: "Кніга:" STR_BOOK_PREFIX: "Кніга:"
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL" STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
STR_PERCENT_STEP_HINT: "Улева/Управа: 1% Уверх/Уніз: 10%"
STR_SYNCING_TIME: "Сінхранізацыя часу..." STR_SYNCING_TIME: "Сінхранізацыя часу..."
STR_CALC_HASH: "Вылічэнне хэша дакумента..." STR_CALC_HASH: "Вылічэнне хэша дакумента..."
STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента" STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента"
@@ -291,7 +297,8 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак"
STR_LINK: "[спасылка]" STR_LINK: "[спасылка]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв" STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі" STR_SLEEP_NEVER: "Ніколі"
STR_SLEEP_TIMER_STEP_HINT: "Улева/Управа: 1 хв Уверх/Уніз: 5 хв" STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
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: "Перагортванне нахілам"
+13 -4
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Mode de pantalla de 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_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Ràpid"
STR_IMAGES: "Imatges" STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_PLACEHOLDER: "Text de mostra"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el 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_FONT_FAMILY: "Tipus de lletra" STR_FONT_FAMILY: "Tipus de lletra"
STR_FONT_SIZE: "Mida de la lletra (UI)" STR_FONT_SIZE: "Mida de la lletra (UI)"
STR_LINE_SPACING: "Interlineat del lector" STR_LINE_SPACING: "Interlineat del lector"
@@ -135,9 +138,12 @@ STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Punt de llibre"
STR_DISABLED: "Desactivats" STR_DISABLED: "Desactivats"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
@@ -172,8 +178,7 @@ 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_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar" STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre."
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 feed"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
@@ -189,6 +194,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_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
STR_CONNECT: "Connecta" STR_CONNECT: "Connecta"
@@ -233,6 +239,7 @@ STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
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."
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS" 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"
@@ -267,7 +274,6 @@ 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, afegiu /opds a la URL"
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
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 el hash del document..."
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document" STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
@@ -294,12 +300,14 @@ STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat" STR_EMBEDDED_STYLE: "Estil incrustat"
STR_FOCUS_READING: "Lectura enfocada" STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorn ràpid des de les notes al peu"
STR_FOOTNOTES: "Notes al peu" STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina" STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
STR_LINK: "[enllaç]" STR_LINK: "[enllaç]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Mai" STR_SLEEP_NEVER: "Mai"
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min" STR_STEP_HINT_FRONT: "Botons frontals:"
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: "Passar automàtic activat: "
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
@@ -338,6 +346,7 @@ STR_MANAGE_FONTS: "Gestiona els tipus de lletra"
STR_FONT_BROWSER: "Navegador de tipus de lletra" STR_FONT_BROWSER: "Navegador de tipus de lletra"
STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..." STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..."
STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles" STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles"
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: "Tipus de lletra instal·lat!"
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra" STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra"
STR_INSTALLED: "Instal·lat" STR_INSTALLED: "Instal·lat"
+10 -3
View File
@@ -58,13 +58,15 @@ STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“" STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
STR_CAT_DISPLAY: "Displej" STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čtečka" STR_CAT_READER: "Čtečka"
STR_CAT_CONTROLS: "Ovládací prvky" STR_CAT_CONTROLS: "Ovládání"
STR_CAT_SYSTEM: "Systém" STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku" STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu" STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
STR_HIDE_BATTERY: "Skrýt baterii %" STR_HIDE_BATTERY: "Skrýt baterii %"
STR_EXTRA_SPACING: "Extra mezery mezi odstavci" STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
STR_TEXT_AA: "Vyhlazování textu" STR_TEXT_AA: "Vyhlazování textu"
STR_TEXT_AA_FULL: "Plné"
STR_TEXT_AA_FAST: "Rychlé"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení" STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení" STR_ORIENTATION: "Orientace čtení"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)" STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
@@ -73,6 +75,7 @@ STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP" STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly" STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace"
STR_FONT_PREVIEW_TEXT: "Příliš žluťoučký kůň úpěl ďábelské ódy"
STR_FONT_FAMILY: "Rodina písem čtečky" STR_FONT_FAMILY: "Rodina písem čtečky"
STR_FONT_SIZE: "Velikost písma rozhraní" STR_FONT_SIZE: "Velikost písma rozhraní"
STR_LINE_SPACING: "Řádkování čtečky" STR_LINE_SPACING: "Řádkování čtečky"
@@ -130,6 +133,7 @@ STR_PAGE_TURN: "Otáčení stránek"
STR_PORTRAIT: "Na výšku" STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček" STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný" STR_INVERTED: "Invertovaný"
STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_PREV_NEXT: "Předchozí/Další" STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí" STR_NEXT_PREV: "Další/Předchozí"
@@ -180,6 +184,9 @@ STR_EXIT: "« Konec"
STR_HOME: "« Domů" STR_HOME: "« Domů"
STR_SELECT: "Vybrat" STR_SELECT: "Vybrat"
STR_TOGGLE: "Přepnout" STR_TOGGLE: "Přepnout"
STR_TOGGLE_BOOKMARK: "Přepnout záložku"
STR_BOOKMARK_REMOVED: "Záložka odstraněna."
STR_HOLD_OPEN_TO_DELETE: "Podržte Otevřít pro smazání"
STR_CONFIRM: "Potvrdit" STR_CONFIRM: "Potvrdit"
STR_CANCEL: "Zrušit" STR_CANCEL: "Zrušit"
STR_CONNECT: "Připojit" STR_CONNECT: "Připojit"
@@ -238,7 +245,6 @@ STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |" STR_PAGES_SEPARATOR: "stránek |"
STR_BOOK_PREFIX: "Kniha:" STR_BOOK_PREFIX: "Kniha:"
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy" STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %"
STR_SYNCING_TIME: "Čas synchronizace..." STR_SYNCING_TIME: "Čas synchronizace..."
STR_CALC_HASH: "Výpočet hashe dokumentu..." STR_CALC_HASH: "Výpočet hashe dokumentu..."
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu" STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
@@ -268,5 +274,6 @@ STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy" STR_SLEEP_NEVER: "Nikdy"
STR_SLEEP_TIMER_STEP_HINT: "Vlevo/Vpravo: 1 min Nahoru/Dolů: 5 min" STR_STEP_HINT_FRONT: "Přední 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"
+9 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Hvile-skærm omslag-tilstand"
STR_HIDE_BATTERY: "Skjul batteri %" STR_HIDE_BATTERY: "Skjul batteri %"
STR_EXTRA_SPACING: "Ekstra afsnitsafstand" STR_EXTRA_SPACING: "Ekstra afsnitsafstand"
STR_TEXT_AA: "Tekst Anti-Aliasing" STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_TEXT_AA_FULL: "Fuld"
STR_TEXT_AA_FAST: "Hurtig"
STR_IMAGES: "Billeder" STR_IMAGES: "Billeder"
STR_IMAGES_DISPLAY: "Vis" STR_IMAGES_DISPLAY: "Vis"
STR_IMAGES_PLACEHOLDER: "Pladsholder" STR_IMAGES_PLACEHOLDER: "Pladsholder"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación"
STR_FONT_PREVIEW_TEXT: "Høj bly gom vandt fræk sexquiz på wc"
STR_FONT_FAMILY: "Læser skrifttype" STR_FONT_FAMILY: "Læser skrifttype"
STR_FONT_SIZE: "Læser skriftstørrelse" STR_FONT_SIZE: "Læser skriftstørrelse"
STR_LINE_SPACING: "Linjeafstand" STR_LINE_SPACING: "Linjeafstand"
@@ -135,6 +138,7 @@ STR_PAGE_TURN: "Sideskift"
STR_PORTRAIT: "Portræt" STR_PORTRAIT: "Portræt"
STR_LANDSCAPE_CW: "Liggende med uret" STR_LANDSCAPE_CW: "Liggende med uret"
STR_INVERTED: "Inverteret" STR_INVERTED: "Inverteret"
STR_ORIENTATION_INVERTED: "Portræt 180°"
STR_LANDSCAPE_CCW: "Liggende mod uret" STR_LANDSCAPE_CCW: "Liggende mod uret"
STR_PREV_NEXT: "Forrige/Næste" STR_PREV_NEXT: "Forrige/Næste"
STR_NEXT_PREV: "Næste/Forrige" STR_NEXT_PREV: "Næste/Forrige"
@@ -186,6 +190,9 @@ STR_HOME: "« Hjem"
STR_SELECT: "Vælg" STR_SELECT: "Vælg"
STR_SELECTED: "Valgt" STR_SELECTED: "Valgt"
STR_TOGGLE: "Skift" STR_TOGGLE: "Skift"
STR_TOGGLE_BOOKMARK: "Skift bogmærke"
STR_BOOKMARK_REMOVED: "Bogmærke fjernet."
STR_HOLD_OPEN_TO_DELETE: "Hold Åbn nede for at slette"
STR_CONFIRM: "Bekræft" STR_CONFIRM: "Bekræft"
STR_CANCEL: "Annuller" STR_CANCEL: "Annuller"
STR_CONNECT: "Forbind" STR_CONNECT: "Forbind"
@@ -261,7 +268,6 @@ STR_CHAPTER_PREFIX: "Kapitel: "
STR_PAGES_SEPARATOR: " sider | " STR_PAGES_SEPARATOR: " sider | "
STR_BOOK_PREFIX: "Bog: " STR_BOOK_PREFIX: "Bog: "
STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre" STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre"
STR_PERCENT_STEP_HINT: "Venstre/Højre: 1% Op/Ned: 10%"
STR_SYNCING_TIME: "Synkroniserer tid..." STR_SYNCING_TIME: "Synkroniserer tid..."
STR_CALC_HASH: "Beregner dokument-hash..." STR_CALC_HASH: "Beregner dokument-hash..."
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash" STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
@@ -293,7 +299,8 @@ STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig" STR_SLEEP_NEVER: "Aldrig"
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Højre: 1 min Op/Ned: 5 min" STR_STEP_HINT_FRONT: "Frontknapper:"
STR_STEP_HINT_SIDE: "Sideknapper:"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede" 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)"
+10 -3
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Slaapscherm omslag-modus"
STR_HIDE_BATTERY: "Batterij % verbergen" STR_HIDE_BATTERY: "Batterij % verbergen"
STR_EXTRA_SPACING: "Extra regelafstand alinea" STR_EXTRA_SPACING: "Extra regelafstand alinea"
STR_TEXT_AA: "Tekst Anti-Aliasing" STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_TEXT_AA_FULL: "Volledig"
STR_TEXT_AA_FAST: "Snel"
STR_IMAGES: "Afbeeldingen" STR_IMAGES: "Afbeeldingen"
STR_IMAGES_DISPLAY: "Weergave" STR_IMAGES_DISPLAY: "Weergave"
STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_PLACEHOLDER: "Placeholder"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_PREVIEW_TEXT: "Pa's wijze lynx bezag vroom het fikse aquaduct"
STR_FONT_FAMILY: "Lettertype lezer" STR_FONT_FAMILY: "Lettertype lezer"
STR_FONT_SIZE: "Lettergrootte lezer" STR_FONT_SIZE: "Lettergrootte lezer"
STR_LINE_SPACING: "Regelafstand lezer" STR_LINE_SPACING: "Regelafstand lezer"
@@ -134,7 +137,8 @@ STR_SLEEP: "Slaap"
STR_PAGE_TURN: "Pagina omslaan" STR_PAGE_TURN: "Pagina omslaan"
STR_PORTRAIT: "Staand" STR_PORTRAIT: "Staand"
STR_LANDSCAPE_CW: "Liggend (rechtsom)" STR_LANDSCAPE_CW: "Liggend (rechtsom)"
STR_INVERTED: "Omgekeerd" STR_INVERTED: "Geïnverteerd"
STR_ORIENTATION_INVERTED: "Staand 180°"
STR_LANDSCAPE_CCW: "Liggend (linksom)" STR_LANDSCAPE_CCW: "Liggend (linksom)"
STR_PREV_NEXT: "Vorige/Volgende" STR_PREV_NEXT: "Vorige/Volgende"
STR_NEXT_PREV: "Volgende/Vorige" STR_NEXT_PREV: "Volgende/Vorige"
@@ -186,6 +190,9 @@ STR_HOME: "« Home"
STR_SELECT: "Kies" STR_SELECT: "Kies"
STR_SELECTED: "Geselecteerd" STR_SELECTED: "Geselecteerd"
STR_TOGGLE: "Wissel" STR_TOGGLE: "Wissel"
STR_TOGGLE_BOOKMARK: "Bladwijzer wisselen"
STR_BOOKMARK_REMOVED: "Bladwijzer verwijderd."
STR_HOLD_OPEN_TO_DELETE: "Houd Openen ingedrukt om te verwijderen"
STR_CONFIRM: "Bevestig" STR_CONFIRM: "Bevestig"
STR_CANCEL: "Annuleer" STR_CANCEL: "Annuleer"
STR_CONNECT: "Verbind" STR_CONNECT: "Verbind"
@@ -261,7 +268,6 @@ STR_CHAPTER_PREFIX: "Hoofdstuk: "
STR_PAGES_SEPARATOR: " pagina's | " STR_PAGES_SEPARATOR: " pagina's | "
STR_BOOK_PREFIX: "Boek: " STR_BOOK_PREFIX: "Boek: "
STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL" STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL"
STR_PERCENT_STEP_HINT: "Links/Rechts: 1% Omhoog/Omlaag: 10%"
STR_SYNCING_TIME: "Tijd synchroniseren..." STR_SYNCING_TIME: "Tijd synchroniseren..."
STR_CALC_HASH: "Document-hash berekenen..." STR_CALC_HASH: "Document-hash berekenen..."
STR_HASH_FAILED: "Document-hash berekenen mislukt" STR_HASH_FAILED: "Document-hash berekenen mislukt"
@@ -293,7 +299,8 @@ STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nooit" STR_SLEEP_NEVER: "Nooit"
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 min Omhoog/Omlaag: 5 min" STR_STEP_HINT_FRONT: "Voorknoppen:"
STR_STEP_HINT_SIDE: "Zijknoppen:"
STR_SCREENSHOT_BUTTON: "Screenshot maken" 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)"
+14 -4
View File
@@ -66,10 +66,14 @@ STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
STR_HIDE_BATTERY: "Hide Battery %" STR_HIDE_BATTERY: "Hide Battery %"
STR_EXTRA_SPACING: "Extra Paragraph Spacing" STR_EXTRA_SPACING: "Extra Paragraph Spacing"
STR_TEXT_AA: "Text Anti-Aliasing" STR_TEXT_AA: "Text Anti-Aliasing"
STR_TEXT_AA_FULL: "Full"
STR_TEXT_AA_FAST: "Fast"
STR_IMAGES: "Images" STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Display" STR_IMAGES_DISPLAY: "Display"
STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_PLACEHOLDER: "Placeholder"
STR_IMAGES_SUPPRESS: "Suppress" STR_IMAGES_SUPPRESS: "Suppress"
STR_EOB_HOME: "Home"
STR_EOB_CONTINUE_WITH: "Continue with"
STR_SHORT_PWR_BTN: "Short Power Button Click" STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation" STR_ORIENTATION: "Reading Orientation"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)" STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
@@ -78,6 +82,8 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_LONG_PRESS_MENU: "Long-press Menu"
STR_FONT_PREVIEW_TEXT: "The quick brown fox jumps over the lazy dog"
STR_FONT_FAMILY: "Reader Font Family" STR_FONT_FAMILY: "Reader Font Family"
STR_FONT_SIZE: "Reader Font Size" STR_FONT_SIZE: "Reader Font Size"
STR_LINE_SPACING: "Reader Line Spacing" STR_LINE_SPACING: "Reader Line Spacing"
@@ -137,9 +143,12 @@ STR_FORCE_REFRESH: "Refresh Screen"
STR_PORTRAIT: "Portrait" STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW" STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted" STR_INVERTED: "Inverted"
STR_ORIENTATION_INVERTED: "Portrait 180°"
STR_LANDSCAPE_CCW: "Landscape CCW" STR_LANDSCAPE_CCW: "Landscape CCW"
STR_PREV_NEXT: "Prev/Next" STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev" STR_NEXT_PREV: "Next/Prev"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bookmark"
STR_DISABLED: "Disabled" STR_DISABLED: "Disabled"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
@@ -175,8 +184,7 @@ STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed" STR_DOWNLOAD_FAILED: "Download failed"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Unnamed" STR_UNNAMED: "Unnamed"
STR_HOLD_CONFIRM_TO_DELETE: "Hold Confirm to Delete" STR_HOLD_OPEN_TO_DELETE: "Hold Open to Delete"
STR_BOOKMARK_INSTRUCTIONS: "Hold Confirm from the reader to create a bookmark."
STR_NO_SERVER_URL: "No server URL configured" STR_NO_SERVER_URL: "No server URL configured"
STR_FETCH_FEED_FAILED: "Failed to fetch feed" STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed" STR_PARSE_FEED_FAILED: "Failed to parse feed"
@@ -194,6 +202,7 @@ STR_HOME: "« Home"
STR_SELECT: "Select" STR_SELECT: "Select"
STR_SELECTED: "Selected" STR_SELECTED: "Selected"
STR_TOGGLE: "Toggle" STR_TOGGLE: "Toggle"
STR_TOGGLE_BOOKMARK: "Toggle Bookmark"
STR_CONFIRM: "Confirm" STR_CONFIRM: "Confirm"
STR_CANCEL: "Cancel" STR_CANCEL: "Cancel"
STR_CONNECT: "Connect" STR_CONNECT: "Connect"
@@ -256,6 +265,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_BOOKMARKS: "Bookmarks" STR_BOOKMARKS: "Bookmarks"
STR_BOOKMARK_ADDED: "Bookmark added." STR_BOOKMARK_ADDED: "Bookmark added."
STR_BOOKMARK_REMOVED: "Bookmark removed."
STR_OPDS_BROWSER: "OPDS Browser" STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Search" STR_SEARCH: "Search"
STR_COVER_CUSTOM: "Cover + Custom" STR_COVER_CUSTOM: "Cover + Custom"
@@ -291,7 +301,6 @@ STR_CHAPTER_PREFIX: "Chapter: "
STR_PAGES_SEPARATOR: " pages | " STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Book: " STR_BOOK_PREFIX: "Book: "
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL" STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%"
STR_SYNCING_TIME: "Syncing time..." STR_SYNCING_TIME: "Syncing time..."
STR_CALC_HASH: "Calculating document hash..." STR_CALC_HASH: "Calculating document hash..."
STR_HASH_FAILED: "Failed to calculate document hash" STR_HASH_FAILED: "Failed to calculate document hash"
@@ -326,7 +335,8 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Take screenshot" STR_SCREENSHOT_BUTTON: "Take screenshot"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Never" STR_SLEEP_NEVER: "Never"
STR_SLEEP_TIMER_STEP_HINT: "Left/Right: 1 min Up/Down: 5 min" STR_STEP_HINT_FRONT: "Front buttons:"
STR_STEP_HINT_SIDE: "Side buttons:"
STR_ADD_SERVER: "Add Server" STR_ADD_SERVER: "Add Server"
STR_SERVER_NAME: "Server Name" STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured" STR_NO_SERVERS: "No OPDS servers configured"
+10 -3
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Lepotilanäytön kansitila"
STR_HIDE_BATTERY: "Piilota akun %" STR_HIDE_BATTERY: "Piilota akun %"
STR_EXTRA_SPACING: "Kappaleiden lisäväli" STR_EXTRA_SPACING: "Kappaleiden lisäväli"
STR_TEXT_AA: "Tekstin reunanpehmennys" STR_TEXT_AA: "Tekstin reunanpehmennys"
STR_TEXT_AA_FULL: "Täysi"
STR_TEXT_AA_FAST: "Nopea"
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus" STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
STR_ORIENTATION: "Lukusuunta" STR_ORIENTATION: "Lukusuunta"
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)" STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
@@ -73,6 +75,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_PREVIEW_TEXT: "Törkylempijävongahdus"
STR_FONT_FAMILY: "Lukijan fonttiperhe" STR_FONT_FAMILY: "Lukijan fonttiperhe"
STR_FONT_SIZE: "Käyttöliittymän fonttikoko" STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
STR_LINE_SPACING: "Lukijan riviväli" STR_LINE_SPACING: "Lukijan riviväli"
@@ -129,7 +132,8 @@ STR_SLEEP: "Lepotila"
STR_PAGE_TURN: "Sivunkääntö" STR_PAGE_TURN: "Sivunkääntö"
STR_PORTRAIT: "Pysty" STR_PORTRAIT: "Pysty"
STR_LANDSCAPE_CW: "Vaaka myötäpäivään" STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
STR_INVERTED: "Käännetty" STR_INVERTED: "Käänteinen"
STR_ORIENTATION_INVERTED: "Pysty 180°"
STR_LANDSCAPE_CCW: "Vaaka vastapäivään" STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
STR_PREV_NEXT: "Edell/Seur" STR_PREV_NEXT: "Edell/Seur"
STR_NEXT_PREV: "Seur/Edell" STR_NEXT_PREV: "Seur/Edell"
@@ -180,6 +184,9 @@ STR_EXIT: "« Poistu"
STR_HOME: "« Koti" STR_HOME: "« Koti"
STR_SELECT: "Valitse" STR_SELECT: "Valitse"
STR_TOGGLE: "Vaihda" STR_TOGGLE: "Vaihda"
STR_TOGGLE_BOOKMARK: "Vaihda kirjanmerkki"
STR_BOOKMARK_REMOVED: "Kirjanmerkki poistettu."
STR_HOLD_OPEN_TO_DELETE: "Pidä Avaa painettuna poistaaksesi"
STR_CONFIRM: "Vahvista" STR_CONFIRM: "Vahvista"
STR_CANCEL: "Peruuta" STR_CANCEL: "Peruuta"
STR_CONNECT: "Yhdistä" STR_CONNECT: "Yhdistä"
@@ -236,7 +243,6 @@ STR_CHAPTER_PREFIX: "Luku: "
STR_PAGES_SEPARATOR: " sivua | " STR_PAGES_SEPARATOR: " sivua | "
STR_BOOK_PREFIX: "Kirja: " STR_BOOK_PREFIX: "Kirja: "
STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen" STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen"
STR_PERCENT_STEP_HINT: "Vasen/Oikea: 1% Ylös/Alas: 10%"
STR_SYNCING_TIME: "Synkronoidaan aikaa..." STR_SYNCING_TIME: "Synkronoidaan aikaa..."
STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..." STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..."
STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui" STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui"
@@ -266,5 +272,6 @@ STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan" STR_SLEEP_NEVER: "Ei koskaan"
STR_SLEEP_TIMER_STEP_HINT: "Vasen/Oikea: 1 min Ylös/Alas: 5 min" STR_STEP_HINT_FRONT: "Etupainikkeet:"
STR_STEP_HINT_SIDE: "Sivupainikkeet:"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla" STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
+9 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Mode écran de veille"
STR_HIDE_BATTERY: "Masquer % batterie" STR_HIDE_BATTERY: "Masquer % batterie"
STR_EXTRA_SPACING: "Espacement paragraphes" STR_EXTRA_SPACING: "Espacement paragraphes"
STR_TEXT_AA: "Lissage du texte" STR_TEXT_AA: "Lissage du texte"
STR_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Rapide"
STR_IMAGES: "Images" STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Affichage" STR_IMAGES_DISPLAY: "Affichage"
STR_IMAGES_PLACEHOLDER: "Espace réservé" STR_IMAGES_PLACEHOLDER: "Espace réservé"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé" STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saut de chapitre" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saut de chapitre"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Changement d'orientation" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Changement d'orientation"
STR_FONT_PREVIEW_TEXT: "Portez ce vieux whisky au juge blond qui fume"
STR_FONT_FAMILY: "Police de caractères du lecteur" STR_FONT_FAMILY: "Police de caractères du lecteur"
STR_FONT_SIZE: "Taille police lecteur" STR_FONT_SIZE: "Taille police lecteur"
STR_LINE_SPACING: "Interligne" STR_LINE_SPACING: "Interligne"
@@ -135,6 +138,7 @@ STR_PAGE_TURN: "Page suivante"
STR_PORTRAIT: "Portrait" STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Paysage" STR_LANDSCAPE_CW: "Paysage"
STR_INVERTED: "Inversé" STR_INVERTED: "Inversé"
STR_ORIENTATION_INVERTED: "Portrait 180°"
STR_LANDSCAPE_CCW: "Paysage inversé" STR_LANDSCAPE_CCW: "Paysage inversé"
STR_PREV_NEXT: "Préc/Suiv" STR_PREV_NEXT: "Préc/Suiv"
STR_NEXT_PREV: "Suiv/Préc" STR_NEXT_PREV: "Suiv/Préc"
@@ -186,6 +190,9 @@ STR_HOME: "« Accueil"
STR_SELECT: "OK" STR_SELECT: "OK"
STR_SELECTED: "Sélectionné" STR_SELECTED: "Sélectionné"
STR_TOGGLE: "Modifier" STR_TOGGLE: "Modifier"
STR_TOGGLE_BOOKMARK: "Basculer le marque-page"
STR_BOOKMARK_REMOVED: "Marque-page supprimé."
STR_HOLD_OPEN_TO_DELETE: "Maintenir Ouvrir pour supprimer"
STR_CONFIRM: "Confirmer" STR_CONFIRM: "Confirmer"
STR_CANCEL: "Annuler" STR_CANCEL: "Annuler"
STR_CONNECT: "Connecter" STR_CONNECT: "Connecter"
@@ -262,7 +269,6 @@ STR_CHAPTER_PREFIX: "Chapitre : "
STR_PAGES_SEPARATOR: " pages | " STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Livre : " STR_BOOK_PREFIX: "Livre : "
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à lURL" STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à lURL"
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
STR_SYNCING_TIME: "Synchro de lheure…" STR_SYNCING_TIME: "Synchro de lheure…"
STR_CALC_HASH: "Calcul hash doc…" STR_CALC_HASH: "Calcul hash doc…"
STR_HASH_FAILED: "Échec calcul hash" STR_HASH_FAILED: "Échec calcul hash"
@@ -294,7 +300,8 @@ STR_NO_FOOTNOTES: "Aucune note sur cette page"
STR_LINK: "[lien]" STR_LINK: "[lien]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais" STR_SLEEP_NEVER: "Jamais"
STR_SLEEP_TIMER_STEP_HINT: "Gauche/Droite : 1 min Haut/Bas : 5 min" STR_STEP_HINT_FRONT: "Boutons avant :"
STR_STEP_HINT_SIDE: "Boutons latéraux :"
STR_SCREENSHOT_BUTTON: "Capture d'écran" 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)"
+13 -7
View File
@@ -65,21 +65,25 @@ STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
STR_HIDE_BATTERY: "Batterie % ausblenden" STR_HIDE_BATTERY: "Batterie % ausblenden"
STR_EXTRA_SPACING: "Absatzabstand" STR_EXTRA_SPACING: "Absatzabstand"
STR_TEXT_AA: "Schriftglättung" STR_TEXT_AA: "Schriftglättung"
STR_TEXT_AA_FULL: "Voll"
STR_TEXT_AA_FAST: "Schnell"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken" STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung" STR_ORIENTATION: "Leseausrichtung"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)" STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck" STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
STR_LONG_PRESS_BEHAVIOR_OFF: "AUS" STR_LONG_PRESS_BEHAVIOR_OFF: "Aus"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen" STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"
STR_FONT_FAMILY: "Lese-Schriftfamilie" STR_FONT_FAMILY: "Lese-Schriftfamilie"
STR_FONT_SIZE: "Schriftgröße" STR_FONT_SIZE: "Schriftgröße"
STR_LINE_SPACING: "Lese-Zeilenabstand" STR_LINE_SPACING: "Lese-Zeilenabstand"
STR_SCREEN_MARGIN: "Lese-Seitenränder" STR_SCREEN_MARGIN: "Lese-Seitenränder"
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung" STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
STR_LONG_PRESS_MENU: "Menütaste lang drücken"
STR_HYPHENATION: "Silbentrennung" STR_HYPHENATION: "Silbentrennung"
STR_TIME_TO_SLEEP: "Standby nach" STR_TIME_TO_SLEEP: "Standby-Modus nach"
STR_REFRESH_FREQ: "Anti-Ghosting nach" STR_REFRESH_FREQ: "Anti-Ghosting nach"
STR_KOREADER_SYNC: "KOReader-Synchr." STR_KOREADER_SYNC: "KOReader-Synchr."
STR_CHECK_UPDATES: "Nach Updates suchen" STR_CHECK_UPDATES: "Nach Updates suchen"
@@ -128,7 +132,8 @@ STR_PAGE_TURN: "Umblättern"
STR_FORCE_REFRESH: "Bildschirm regenerieren" STR_FORCE_REFRESH: "Bildschirm regenerieren"
STR_PORTRAIT: "Hochformat" STR_PORTRAIT: "Hochformat"
STR_LANDSCAPE_CW: "Querformat rechts" STR_LANDSCAPE_CW: "Querformat rechts"
STR_INVERTED: "Hochformat 180°" STR_INVERTED: "Invertiert"
STR_ORIENTATION_INVERTED: "Hochformat 180°"
STR_LANDSCAPE_CCW: "Querformat links" STR_LANDSCAPE_CCW: "Querformat links"
STR_PREV_NEXT: "Zurück/Weiter" STR_PREV_NEXT: "Zurück/Weiter"
STR_NEXT_PREV: "Weiter/Zurück" STR_NEXT_PREV: "Weiter/Zurück"
@@ -167,8 +172,7 @@ STR_DOWNLOADING: "Herunterladen…"
STR_DOWNLOAD_FAILED: "Ladefehler" STR_DOWNLOAD_FAILED: "Ladefehler"
STR_ERROR_MSG: "Fehler:" STR_ERROR_MSG: "Fehler:"
STR_UNNAMED: "Unbenannt" STR_UNNAMED: "Unbenannt"
STR_HOLD_CONFIRM_TO_DELETE: "Halte Bestätigen zum Löschen" STR_HOLD_OPEN_TO_DELETE: "Halte Öffnen zum Löschen"
STR_BOOKMARK_INSTRUCTIONS: "Halte Bestätigen im Lesemodus um ein Lesezeichen anzulegen."
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert" STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
STR_FETCH_FEED_FAILED: "Feedfehler" STR_FETCH_FEED_FAILED: "Feedfehler"
STR_PARSE_FEED_FAILED: "Feed-Format ungültig" STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
@@ -186,6 +190,7 @@ STR_HOME: "« Start"
STR_SELECT: "Auswahl" STR_SELECT: "Auswahl"
STR_SELECTED: "Ausgewählt" STR_SELECTED: "Ausgewählt"
STR_TOGGLE: "Ändern" STR_TOGGLE: "Ändern"
STR_TOGGLE_BOOKMARK: "Lesezeichen umschalten"
STR_CONFIRM: "Bestätigen" STR_CONFIRM: "Bestätigen"
STR_CANCEL: "Abbrechen" STR_CANCEL: "Abbrechen"
STR_CONNECT: "Verbinden" STR_CONNECT: "Verbinden"
@@ -249,6 +254,7 @@ STR_QUICK_RESUME_TIMEOUT: "Schnelles Fortsetzen nach Timeout"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_BOOKMARKS: "Lesezeichen" STR_BOOKMARKS: "Lesezeichen"
STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt." STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt."
STR_BOOKMARK_REMOVED: "Lesezeichen entfernt."
STR_SEARCH: "Suche" STR_SEARCH: "Suche"
STR_OPDS_BROWSER: "OPDS-Browser" STR_OPDS_BROWSER: "OPDS-Browser"
STR_COVER_CUSTOM: "Cover + Eigenes" STR_COVER_CUSTOM: "Cover + Eigenes"
@@ -284,7 +290,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " Seiten | " STR_PAGES_SEPARATOR: " Seiten | "
STR_BOOK_PREFIX: "Buch: " STR_BOOK_PREFIX: "Buch: "
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen" STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
STR_SYNCING_TIME: "Zeit synchronisieren…" STR_SYNCING_TIME: "Zeit synchronisieren…"
STR_CALC_HASH: "Dokument-Hash berechnen…" STR_CALC_HASH: "Dokument-Hash berechnen…"
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen" STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
@@ -318,7 +323,8 @@ STR_NO_FOOTNOTES: "Keine Fußnoten auf dieser Seite"
STR_LINK: "[Link]" STR_LINK: "[Link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min." STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min."
STR_SLEEP_NEVER: "Nie" STR_SLEEP_NEVER: "Nie"
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 Min. Hoch/Runter: 5 Min." STR_STEP_HINT_FRONT: "Vordere Tasten:"
STR_STEP_HINT_SIDE: "Seitentasten:"
STR_ADD_SERVER: "Server hinzufügen" STR_ADD_SERVER: "Server hinzufügen"
STR_SERVER_NAME: "Servername" STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert" STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
+16 -4
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "תצוגת כריכה במצב שינה"
STR_HIDE_BATTERY: "הסתר אחוזי סוללה" STR_HIDE_BATTERY: "הסתר אחוזי סוללה"
STR_EXTRA_SPACING: "מרווח פסקאות מוגדל" STR_EXTRA_SPACING: "מרווח פסקאות מוגדל"
STR_TEXT_AA: "החלקת קצוות טקסט" STR_TEXT_AA: "החלקת קצוות טקסט"
STR_TEXT_AA_FULL: "מלאה"
STR_TEXT_AA_FAST: "מהירה"
STR_IMAGES: "תמונות" STR_IMAGES: "תמונות"
STR_IMAGES_DISPLAY: "הצג תמונות" STR_IMAGES_DISPLAY: "הצג תמונות"
STR_IMAGES_PLACEHOLDER: "סמן" STR_IMAGES_PLACEHOLDER: "סמן"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי" STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק" STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "שנה כיוון מסך" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "שנה כיוון מסך"
STR_FONT_PREVIEW_TEXT: "דג סקרן שט בים מאוכזב ולפתע מצא חברה"
STR_FONT_FAMILY: "גופן הקריאה" STR_FONT_FAMILY: "גופן הקריאה"
STR_FONT_SIZE: "גודל גופן" STR_FONT_SIZE: "גודל גופן"
STR_LINE_SPACING: "מרווח בין שורות" STR_LINE_SPACING: "מרווח בין שורות"
@@ -133,7 +136,8 @@ STR_PAGE_TURN: "העברת דף"
STR_FORCE_REFRESH: "רענון מסך מלא" STR_FORCE_REFRESH: "רענון מסך מלא"
STR_PORTRAIT: "לאורך" STR_PORTRAIT: "לאורך"
STR_LANDSCAPE_CW: "לרוחב (ימינה)" STR_LANDSCAPE_CW: "לרוחב (ימינה)"
STR_INVERTED: "הפוך" STR_INVERTED: יפוך צבעים"
STR_ORIENTATION_INVERTED: "לאורך 180°"
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)" STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
STR_PREV_NEXT: "הקודם/הבא" STR_PREV_NEXT: "הקודם/הבא"
STR_NEXT_PREV: "הבא/הקודם" STR_NEXT_PREV: "הבא/הקודם"
@@ -188,6 +192,7 @@ STR_HOME: "מסך הבית »"
STR_SELECT: "בחר" STR_SELECT: "בחר"
STR_SELECTED: "נבחר" STR_SELECTED: "נבחר"
STR_TOGGLE: "בחר" STR_TOGGLE: "בחר"
STR_TOGGLE_BOOKMARK: "הוסף/הסר סימנייה"
STR_CONFIRM: "אישור" STR_CONFIRM: "אישור"
STR_CANCEL: "ביטול" STR_CANCEL: "ביטול"
STR_CONNECT: "התחבר" STR_CONNECT: "התחבר"
@@ -265,7 +270,6 @@ STR_CHAPTER_PREFIX: "פרק: "
STR_PAGES_SEPARATOR: " דפים | " STR_PAGES_SEPARATOR: " דפים | "
STR_BOOK_PREFIX: "ספר: " STR_BOOK_PREFIX: "ספר: "
STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL" STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL"
STR_PERCENT_STEP_HINT: "שמאל/ימין: 1% למעלה/למטה: 10%"
STR_SYNCING_TIME: "מסנכרן שעון..." STR_SYNCING_TIME: "מסנכרן שעון..."
STR_CALC_HASH: "מחשב האש..." STR_CALC_HASH: "מחשב האש..."
STR_HASH_FAILED: "חישוב האש נכשל" STR_HASH_FAILED: "חישוב האש נכשל"
@@ -359,8 +363,7 @@ STR_FRONT_BTN_FOLLOW_ORIENTATION: "התאמת לחצנים קדמיים לכיו
STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים" STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים"
STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'" STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'"
STR_DISABLED: "מבוטל" STR_DISABLED: "מבוטל"
STR_HOLD_CONFIRM_TO_DELETE: "החזק לחוץ על אישור כדי למחוק" STR_HOLD_OPEN_TO_DELETE: "לחיצה ארוכה על 'פתח' כדי למחוק"
STR_BOOKMARK_INSTRUCTIONS: "החזק לחוץ על כפתור אישור בזמן הקריאה כדי להוסיף סימנייה"
STR_CLOCK: "שעון" STR_CLOCK: "שעון"
STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC" STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC"
STR_CLOCK_FORMAT: "תבנית השעון" STR_CLOCK_FORMAT: "תבנית השעון"
@@ -378,6 +381,15 @@ STR_CLOCK_SYNC_NO_WIFI_HINT: "התחבר תחילה לרשת אלחוטית, ו
STR_CLOCK_SYNCED: "השעון סונכרן" STR_CLOCK_SYNCED: "השעון סונכרן"
STR_BOOKMARKS: "סימניות" STR_BOOKMARKS: "סימניות"
STR_BOOKMARK_ADDED: "הסימנייה התווספה" STR_BOOKMARK_ADDED: "הסימנייה התווספה"
STR_BOOKMARK_REMOVED: "הסימנייה הוסרה"
STR_QUICK_RESUME: "חזרה מהירה" STR_QUICK_RESUME: "חזרה מהירה"
STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?" STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?"
STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?" STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?"
STR_LONG_PRESS_MENU: "לחיצה ארוכה על אישור"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "סימנייה"
STR_PWR_BTN_FOOTNOTE_BACK: "חזרה מהירה מהערות שוליים"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
STR_SLEEP_NEVER: "אף פעם"
STR_STEP_HINT_FRONT: "לחצנים קדמיים:"
STR_STEP_HINT_SIDE: "לחצני צד:"
+10 -3
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Alvásképernyő borítómód"
STR_HIDE_BATTERY: "Akkumulátor % elrejtése" STR_HIDE_BATTERY: "Akkumulátor % elrejtése"
STR_EXTRA_SPACING: "Extra bekezdésköz" STR_EXTRA_SPACING: "Extra bekezdésköz"
STR_TEXT_AA: "Szöveg élsimítás" STR_TEXT_AA: "Szöveg élsimítás"
STR_TEXT_AA_FULL: "Teljes"
STR_TEXT_AA_FAST: "Gyors"
STR_IMAGES: "Képek" STR_IMAGES: "Képek"
STR_IMAGES_DISPLAY: "Megjelenítés" STR_IMAGES_DISPLAY: "Megjelenítés"
STR_IMAGES_PLACEHOLDER: "Helyőrző" STR_IMAGES_PLACEHOLDER: "Helyőrző"
@@ -74,6 +76,7 @@ STR_ORIENTATION: "Olvasási irány"
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)" STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás" STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
STR_FONT_PREVIEW_TEXT: "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban"
STR_FONT_FAMILY: "Olvasó betűkészlet" STR_FONT_FAMILY: "Olvasó betűkészlet"
STR_FONT_SIZE: "Olvasó betűméret" STR_FONT_SIZE: "Olvasó betűméret"
STR_LINE_SPACING: "Olvasó sorköz" STR_LINE_SPACING: "Olvasó sorköz"
@@ -131,7 +134,8 @@ STR_SLEEP: "Alvás"
STR_PAGE_TURN: "Lapozás" STR_PAGE_TURN: "Lapozás"
STR_PORTRAIT: "Álló" STR_PORTRAIT: "Álló"
STR_LANDSCAPE_CW: "Fekvő jobbra" STR_LANDSCAPE_CW: "Fekvő jobbra"
STR_INVERTED: "Fordított" STR_INVERTED: "Invertált"
STR_ORIENTATION_INVERTED: "Álló 180°"
STR_LANDSCAPE_CCW: "Fekvő balra" STR_LANDSCAPE_CCW: "Fekvő balra"
STR_PREV_NEXT: "Előző/Következő" STR_PREV_NEXT: "Előző/Következő"
STR_NEXT_PREV: "Következő/Előző" STR_NEXT_PREV: "Következő/Előző"
@@ -183,6 +187,9 @@ STR_HOME: "« Főoldal"
STR_SELECT: "Kiválasztás" STR_SELECT: "Kiválasztás"
STR_SELECTED: "Kiválasztva" STR_SELECTED: "Kiválasztva"
STR_TOGGLE: "Váltás" STR_TOGGLE: "Váltás"
STR_TOGGLE_BOOKMARK: "Könyvjelző váltása"
STR_BOOKMARK_REMOVED: "Könyvjelző eltávolítva."
STR_HOLD_OPEN_TO_DELETE: "Tartsa lenyomva a Megnyitás gombot a törléshez"
STR_CONFIRM: "Megerősítés" STR_CONFIRM: "Megerősítés"
STR_CANCEL: "Mégse" STR_CANCEL: "Mégse"
STR_CONNECT: "Csatlakozás" STR_CONNECT: "Csatlakozás"
@@ -258,7 +265,6 @@ STR_CHAPTER_PREFIX: "Fejezet: "
STR_PAGES_SEPARATOR: " oldal | " STR_PAGES_SEPARATOR: " oldal | "
STR_BOOK_PREFIX: "Könyv: " STR_BOOK_PREFIX: "Könyv: "
STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez" STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez"
STR_PERCENT_STEP_HINT: "Bal/Jobb: 1% Fel/Le: 10%"
STR_SYNCING_TIME: "Idő szinkronizálása..." STR_SYNCING_TIME: "Idő szinkronizálása..."
STR_CALC_HASH: "Dokumentum hash kiszámítása..." STR_CALC_HASH: "Dokumentum hash kiszámítása..."
STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen" STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen"
@@ -290,7 +296,8 @@ STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc" STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha" STR_SLEEP_NEVER: "Soha"
STR_SLEEP_TIMER_STEP_HINT: "Bal/Jobb: 1 perc Fel/Le: 5 perc" STR_STEP_HINT_FRONT: "Elülső gombok:"
STR_STEP_HINT_SIDE: "Oldalsó gombok:"
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése" 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)"
+15 -6
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Modalità copertina"
STR_HIDE_BATTERY: "Nascondi % batteria" STR_HIDE_BATTERY: "Nascondi % batteria"
STR_EXTRA_SPACING: "Spaziatura extra paragrafi" STR_EXTRA_SPACING: "Spaziatura extra paragrafi"
STR_TEXT_AA: "Anti-aliasing testo" STR_TEXT_AA: "Anti-aliasing testo"
STR_TEXT_AA_FULL: "Completo"
STR_TEXT_AA_FAST: "Veloce"
STR_IMAGES: "Immagini" STR_IMAGES: "Immagini"
STR_IMAGES_DISPLAY: "Visualizza" STR_IMAGES_DISPLAY: "Visualizza"
STR_IMAGES_PLACEHOLDER: "Segnaposto" STR_IMAGES_PLACEHOLDER: "Segnaposto"
@@ -77,6 +79,8 @@ STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Salta capitolo" STR_LONG_PRESS_BEHAVIOR_SKIP: "Salta capitolo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientamento" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientamento"
STR_LONG_PRESS_MENU: "Menu press. lunga"
STR_FONT_PREVIEW_TEXT: "Pranzo d'acqua fa volti sghembi"
STR_FONT_FAMILY: "Font lettore" STR_FONT_FAMILY: "Font lettore"
STR_FONT_SIZE: "Dimensione font" STR_FONT_SIZE: "Dimensione font"
STR_LINE_SPACING: "Interlinea lettore" STR_LINE_SPACING: "Interlinea lettore"
@@ -135,7 +139,8 @@ STR_PAGE_TURN: "Cambio pagina"
STR_FORCE_REFRESH: "Refresh" STR_FORCE_REFRESH: "Refresh"
STR_PORTRAIT: "Verticale" STR_PORTRAIT: "Verticale"
STR_LANDSCAPE_CW: "Orizzontale Dx" STR_LANDSCAPE_CW: "Orizzontale Dx"
STR_INVERTED: "Capovolto" STR_INVERTED: "Invertito"
STR_ORIENTATION_INVERTED: "Verticale 180°"
STR_LANDSCAPE_CCW: "Orizzontale Sx" STR_LANDSCAPE_CCW: "Orizzontale Sx"
STR_PREV_NEXT: "Prec/Succ" STR_PREV_NEXT: "Prec/Succ"
STR_NEXT_PREV: "Succ/Prec" STR_NEXT_PREV: "Succ/Prec"
@@ -190,6 +195,7 @@ STR_HOME: "« Home"
STR_SELECT: "Seleziona" STR_SELECT: "Seleziona"
STR_SELECTED: "Selezionato" STR_SELECTED: "Selezionato"
STR_TOGGLE: "Cambia" STR_TOGGLE: "Cambia"
STR_TOGGLE_BOOKMARK: "Attiva/disattiva segnalibro"
STR_CONFIRM: "Conferma" STR_CONFIRM: "Conferma"
STR_CANCEL: "Annulla" STR_CANCEL: "Annulla"
STR_CONNECT: "Connetti" STR_CONNECT: "Connetti"
@@ -270,7 +276,6 @@ STR_CHAPTER_PREFIX: "Capitolo: "
STR_PAGES_SEPARATOR: " pagine | " STR_PAGES_SEPARATOR: " pagine | "
STR_BOOK_PREFIX: "Libro: " STR_BOOK_PREFIX: "Libro: "
STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL" STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL"
STR_PERCENT_STEP_HINT: "Sinistra/Destra: 1% Su/Giù: 10%"
STR_SYNCING_TIME: "Sincronizzazione orario..." STR_SYNCING_TIME: "Sincronizzazione orario..."
STR_CALC_HASH: "Calcolo hash documento..." STR_CALC_HASH: "Calcolo hash documento..."
STR_HASH_FAILED: "Impossibile calcolare l'hash del documento" STR_HASH_FAILED: "Impossibile calcolare l'hash del documento"
@@ -303,7 +308,8 @@ STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Mai" STR_SLEEP_NEVER: "Mai"
STR_SLEEP_TIMER_STEP_HINT: "Sinistra/Destra: 1 min Su/Giù: 5 min" STR_STEP_HINT_FRONT: "Pulsanti frontali:"
STR_STEP_HINT_SIDE: "Pulsanti laterali:"
STR_SCREENSHOT_BUTTON: "Screenshot" STR_SCREENSHOT_BUTTON: "Screenshot"
STR_ADD_SERVER: "Aggiungi server" STR_ADD_SERVER: "Aggiungi server"
STR_SERVER_NAME: "Nome server" STR_SERVER_NAME: "Nome server"
@@ -355,9 +361,9 @@ STR_FIRMWARE_WRITE_FAILED: "Aggiornamento firmware non riuscito"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!"
STR_RECOVERY_MODE: "Modalità ripristino" STR_RECOVERY_MODE: "Modalità ripristino"
STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo" STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo"
STR_BOOKMARK_INSTRUCTIONS: "Tieni premuto Conferma nel lettore per creare un segnalibro"
STR_BOOKMARKS: "Segnalibri" STR_BOOKMARKS: "Segnalibri"
STR_BOOKMARK_ADDED: "Segnalibro aggiunto" STR_BOOKMARK_ADDED: "Segnalibro aggiunto"
STR_BOOKMARK_REMOVED: "Segnalibro rimosso"
STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?" STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova" STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova"
STR_CLOCK_SYNC_OK: "Orologio sincronizzato" STR_CLOCK_SYNC_OK: "Orologio sincronizzato"
@@ -372,7 +378,10 @@ STR_CLOCK_SYNCING: "Sincronizzazione con il server NTP..."
STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita" STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita"
STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso" STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso" STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso"
STR_HOLD_CONFIRM_TO_DELETE: "Tieni premuto Conferma per cancellare" STR_HOLD_OPEN_TO_DELETE: "Tieni premuto Apri per eliminare"
STR_NEXT_FIELD: "Succ." STR_NEXT_FIELD: "Succ."
STR_CURRENT_TIME: "Ora attuale: " STR_CURRENT_TIME: "Ora attuale: "
STR_DISABLED: "Disattivato" STR_DISABLED: "Disattivato"
STR_BOOKMARK_OPTION: "Segnalibro"
STR_KOSYNC: "KOSync"
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
+10 -3
View File
@@ -64,11 +64,14 @@ STR_SLEEP_COVER_MODE: "Ұйқы экраны мұқаба режимі"
STR_HIDE_BATTERY: "Батарея % жасыру" STR_HIDE_BATTERY: "Батарея % жасыру"
STR_EXTRA_SPACING: "Қосымша абзац аралығы" STR_EXTRA_SPACING: "Қосымша абзац аралығы"
STR_TEXT_AA: "Мәтін сырғытпасы" STR_TEXT_AA: "Мәтін сырғытпасы"
STR_TEXT_AA_FULL: "Толық"
STR_TEXT_AA_FAST: "Жылдам"
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу" STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
STR_ORIENTATION: "Оқу бағдары" STR_ORIENTATION: "Оқу бағдары"
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)" STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу" STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
STR_FONT_FAMILY: "Оқырман қаріп тобы" STR_FONT_FAMILY: "Оқырман қаріп тобы"
STR_FONT_SIZE: "Интерфейс қаріп өлшемі" STR_FONT_SIZE: "Интерфейс қаріп өлшемі"
STR_LINE_SPACING: "Оқырман жол аралығы" STR_LINE_SPACING: "Оқырман жол аралығы"
@@ -125,7 +128,8 @@ STR_SLEEP: "Ұйқы"
STR_PAGE_TURN: "Бет аудару" STR_PAGE_TURN: "Бет аудару"
STR_PORTRAIT: "Тік бағдар" STR_PORTRAIT: "Тік бағдар"
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)" STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
STR_INVERTED: "Төңкерілген" STR_INVERTED: "Инверсия"
STR_ORIENTATION_INVERTED: "Тік бағдар 180°"
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)" STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
STR_PREV_NEXT: "Алдыңғы/Келесі" STR_PREV_NEXT: "Алдыңғы/Келесі"
STR_NEXT_PREV: "Келесі/Алдыңғы" STR_NEXT_PREV: "Келесі/Алдыңғы"
@@ -176,6 +180,9 @@ STR_EXIT: "« Шығу"
STR_HOME: "« Басты" STR_HOME: "« Басты"
STR_SELECT: "Таңдау" STR_SELECT: "Таңдау"
STR_TOGGLE: "Ауыстыру" STR_TOGGLE: "Ауыстыру"
STR_TOGGLE_BOOKMARK: "Бетбелгіні ауыстыру"
STR_BOOKMARK_REMOVED: "Бетбелгі жойылды."
STR_HOLD_OPEN_TO_DELETE: "Жою үшін Ашу түймесін ұстап тұрыңыз"
STR_CONFIRM: "Растау" STR_CONFIRM: "Растау"
STR_CANCEL: "Болдырмау" STR_CANCEL: "Болдырмау"
STR_CONNECT: "Қосылу" STR_CONNECT: "Қосылу"
@@ -232,7 +239,6 @@ STR_CHAPTER_PREFIX: "Тарау: "
STR_PAGES_SEPARATOR: " бет | " STR_PAGES_SEPARATOR: " бет | "
STR_BOOK_PREFIX: "Кітап: " STR_BOOK_PREFIX: "Кітап: "
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз" STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
STR_PERCENT_STEP_HINT: "Сол/Оң: 1% Жоғары/Төмен: 10%"
STR_SYNCING_TIME: "Уақыт синхрондалуда..." STR_SYNCING_TIME: "Уақыт синхрондалуда..."
STR_CALC_HASH: "Құжат хэші есептелуде..." STR_CALC_HASH: "Құжат хэші есептелуде..."
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз" STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
@@ -289,7 +295,8 @@ STR_NO_FOOTNOTES: "Бұл бетте түсіндірме жазбалар жо
STR_LINK: "[сілтеме]" STR_LINK: "[сілтеме]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин" STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан" STR_SLEEP_NEVER: "Ешқашан"
STR_SLEEP_TIMER_STEP_HINT: "Сол/Оң: 1 мин Жоғары/Төмен: 5 мин" STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
STR_STEP_HINT_SIDE: "Бүйір түймелері:"
STR_SCREENSHOT_BUTTON: "Скриншот түсіру" STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: " STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
+10 -3
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Viršelio režimas"
STR_HIDE_BATTERY: "Slėpti baterijos %" STR_HIDE_BATTERY: "Slėpti baterijos %"
STR_EXTRA_SPACING: "Tarpai tarp pastraipų" STR_EXTRA_SPACING: "Tarpai tarp pastraipų"
STR_TEXT_AA: "Teksto glotninimas" STR_TEXT_AA: "Teksto glotninimas"
STR_TEXT_AA_FULL: "Pilnas"
STR_TEXT_AA_FAST: "Greitas"
STR_IMAGES: "Paveikslėliai" STR_IMAGES: "Paveikslėliai"
STR_IMAGES_DISPLAY: "Rodyti" STR_IMAGES_DISPLAY: "Rodyti"
STR_IMAGES_PLACEHOLDER: "Vietaženklis" STR_IMAGES_PLACEHOLDER: "Vietaženklis"
@@ -74,6 +76,7 @@ STR_ORIENTATION: "Orientacija"
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai" STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)" STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
STR_FONT_FAMILY: "Šriftas" STR_FONT_FAMILY: "Šriftas"
STR_FONT_SIZE: "Šrifto dydis" STR_FONT_SIZE: "Šrifto dydis"
STR_LINE_SPACING: "Tarpai tarp eilučių" STR_LINE_SPACING: "Tarpai tarp eilučių"
@@ -131,7 +134,8 @@ STR_SLEEP: "Miegas"
STR_PAGE_TURN: "Versti psl." STR_PAGE_TURN: "Versti psl."
STR_PORTRAIT: "Stačias" STR_PORTRAIT: "Stačias"
STR_LANDSCAPE_CW: "Gulsčias (P)" STR_LANDSCAPE_CW: "Gulsčias (P)"
STR_INVERTED: "Apverstas" STR_INVERTED: "Invertuotas"
STR_ORIENTATION_INVERTED: "Stačias 180°"
STR_LANDSCAPE_CCW: "Gulsčias (A)" STR_LANDSCAPE_CCW: "Gulsčias (A)"
STR_PREV_NEXT: "Atgal/Pirmyn" STR_PREV_NEXT: "Atgal/Pirmyn"
STR_NEXT_PREV: "Pirmyn/Atgal" STR_NEXT_PREV: "Pirmyn/Atgal"
@@ -183,6 +187,9 @@ STR_HOME: "« Pradžia"
STR_SELECT: "Rinktis" STR_SELECT: "Rinktis"
STR_SELECTED: "Pasirinkta" STR_SELECTED: "Pasirinkta"
STR_TOGGLE: "Keisti" STR_TOGGLE: "Keisti"
STR_TOGGLE_BOOKMARK: "Perjungti žymę"
STR_BOOKMARK_REMOVED: "Žymė pašalinta."
STR_HOLD_OPEN_TO_DELETE: "Laikykite Atidaryti, kad ištrintumėte"
STR_CONFIRM: "Gerai" STR_CONFIRM: "Gerai"
STR_CANCEL: "Atšaukti" STR_CANCEL: "Atšaukti"
STR_CONNECT: "Jungtis" STR_CONNECT: "Jungtis"
@@ -258,7 +265,6 @@ STR_CHAPTER_PREFIX: "Sk: "
STR_PAGES_SEPARATOR: " psl. | " STR_PAGES_SEPARATOR: " psl. | "
STR_BOOK_PREFIX: "Kn: " STR_BOOK_PREFIX: "Kn: "
STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL" STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL"
STR_PERCENT_STEP_HINT: "K/D: 1% | V/A: 10%"
STR_SYNCING_TIME: "Sinchr. laikas..." STR_SYNCING_TIME: "Sinchr. laikas..."
STR_CALC_HASH: "Skaičiuojama maiša..." STR_CALC_HASH: "Skaičiuojama maiša..."
STR_HASH_FAILED: "Maišos klaida" STR_HASH_FAILED: "Maišos klaida"
@@ -290,7 +296,8 @@ STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
STR_LINK: "[nuoroda]" STR_LINK: "[nuoroda]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min." STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Niekada" STR_SLEEP_NEVER: "Niekada"
STR_SLEEP_TIMER_STEP_HINT: "Kairė/Dešinė: 1 min. Viršus/Apačia: 5 min." STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka" 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)"
+10 -3
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Okładki wygaszacza"
STR_HIDE_BATTERY: "Ukryj % baterii" STR_HIDE_BATTERY: "Ukryj % baterii"
STR_EXTRA_SPACING: "Dodatkowe odstępy paragrafów" STR_EXTRA_SPACING: "Dodatkowe odstępy paragrafów"
STR_TEXT_AA: "Wygładzanie tekstu" STR_TEXT_AA: "Wygładzanie tekstu"
STR_TEXT_AA_FULL: "Pełne"
STR_TEXT_AA_FAST: "Szybkie"
STR_IMAGES: "Obrazki" STR_IMAGES: "Obrazki"
STR_IMAGES_DISPLAY: "Pokazuj" STR_IMAGES_DISPLAY: "Pokazuj"
STR_IMAGES_PLACEHOLDER: "Ramki" STR_IMAGES_PLACEHOLDER: "Ramki"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył." STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
STR_LONG_PRESS_BEHAVIOR_SKIP: "Przeskocz rozdział" STR_LONG_PRESS_BEHAVIOR_SKIP: "Przeskocz rozdział"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientacja ekranu" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientacja ekranu"
STR_FONT_PREVIEW_TEXT: "Pchnąć w tę łódź jeża lub ośm skrzyń fig"
STR_FONT_FAMILY: "Czcionka" STR_FONT_FAMILY: "Czcionka"
STR_FONT_SIZE: "Rozmiar czcionki" STR_FONT_SIZE: "Rozmiar czcionki"
STR_LINE_SPACING: "Odstępy między wierszami" STR_LINE_SPACING: "Odstępy między wierszami"
@@ -135,7 +138,8 @@ STR_PAGE_TURN: "Nast. str."
STR_FORCE_REFRESH: "Odśwież ekran" STR_FORCE_REFRESH: "Odśwież ekran"
STR_PORTRAIT: "Pionowo" STR_PORTRAIT: "Pionowo"
STR_LANDSCAPE_CW: "Poziomo P" STR_LANDSCAPE_CW: "Poziomo P"
STR_INVERTED: "Odwrócony" STR_INVERTED: "Inwersja"
STR_ORIENTATION_INVERTED: "Pionowo 180°"
STR_LANDSCAPE_CCW: "Poziomo L" STR_LANDSCAPE_CCW: "Poziomo L"
STR_PREV_NEXT: "Poprz./Nast." STR_PREV_NEXT: "Poprz./Nast."
STR_NEXT_PREV: "Nast./Poprz." STR_NEXT_PREV: "Nast./Poprz."
@@ -190,6 +194,9 @@ STR_HOME: "« Home"
STR_SELECT: "Wybierz" STR_SELECT: "Wybierz"
STR_SELECTED: "Wybrano" STR_SELECTED: "Wybrano"
STR_TOGGLE: "Zmień" STR_TOGGLE: "Zmień"
STR_TOGGLE_BOOKMARK: "Przełącz zakładkę"
STR_BOOKMARK_REMOVED: "Zakładka usunięta."
STR_HOLD_OPEN_TO_DELETE: "Przytrzymaj Otwórz, aby usunąć"
STR_CONFIRM: "Potwierdź" STR_CONFIRM: "Potwierdź"
STR_CANCEL: "Anuluj" STR_CANCEL: "Anuluj"
STR_CONNECT: "Połącz" STR_CONNECT: "Połącz"
@@ -270,7 +277,6 @@ STR_CHAPTER_PREFIX: "Rozdział: "
STR_PAGES_SEPARATOR: " stron | " STR_PAGES_SEPARATOR: " stron | "
STR_BOOK_PREFIX: "Książka: " STR_BOOK_PREFIX: "Książka: "
STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL" STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL"
STR_PERCENT_STEP_HINT: "Lewo/Prawo: 1% Góra/Dół: 10%"
STR_SYNCING_TIME: "Synchronizacja czasu..." STR_SYNCING_TIME: "Synchronizacja czasu..."
STR_CALC_HASH: "Obliczanie sumy kontrolnej..." STR_CALC_HASH: "Obliczanie sumy kontrolnej..."
STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej" STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej"
@@ -303,7 +309,8 @@ STR_NO_FOOTNOTES: "Brak przypisów na tej stronie"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nigdy" STR_SLEEP_NEVER: "Nigdy"
STR_SLEEP_TIMER_STEP_HINT: "Lewo/Prawo: 1 min Góra/Dół: 5 min" STR_STEP_HINT_FRONT: "Przyciski przednie:"
STR_STEP_HINT_SIDE: "Przyciski boczne:"
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu" STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
STR_ADD_SERVER: "Dodaj serwer" STR_ADD_SERVER: "Dodaj serwer"
STR_SERVER_NAME: "Nazwa serwera" STR_SERVER_NAME: "Nazwa serwera"
+140 -25
View File
@@ -19,7 +19,7 @@ STR_END_OF_BOOK: "Fim do livro"
STR_EMPTY_CHAPTER: "Capítulo vazio" STR_EMPTY_CHAPTER: "Capítulo vazio"
STR_INDEXING: "Indexando" STR_INDEXING: "Indexando"
STR_MEMORY_ERROR: "Erro de memória" STR_MEMORY_ERROR: "Erro de memória"
STR_PAGE_LOAD_ERROR: "Erro página" STR_PAGE_LOAD_ERROR: "Erro ao carregar pág."
STR_EMPTY_FILE: "Arquivo vazio" STR_EMPTY_FILE: "Arquivo vazio"
STR_OUT_OF_BOUNDS: "Fora dos limites" STR_OUT_OF_BOUNDS: "Fora dos limites"
STR_LOADING: "Carregando..." STR_LOADING: "Carregando..."
@@ -32,8 +32,8 @@ STR_CONNECTING: "Conectando..."
STR_CONNECTED: "Conectado!" STR_CONNECTED: "Conectado!"
STR_CONNECTION_FAILED: "Falha na conexão" STR_CONNECTION_FAILED: "Falha na conexão"
STR_FORGET_NETWORK: "Esquecer rede?" STR_FORGET_NETWORK: "Esquecer rede?"
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?" STR_SAVE_PASSWORD: "Salvar senha na próxima vez?"
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente" STR_PRESS_OK_SCAN: "Pressione OK para buscar novamente"
STR_JOIN_NETWORK: "Entrar em uma rede" STR_JOIN_NETWORK: "Entrar em uma rede"
STR_CREATE_HOTSPOT: "Criar hotspot" STR_CREATE_HOTSPOT: "Criar hotspot"
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente" STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
@@ -41,7 +41,7 @@ STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas entrarem"
STR_STARTING_HOTSPOT: "Iniciando hotspot..." STR_STARTING_HOTSPOT: "Iniciando hotspot..."
STR_HOTSPOT_MODE: "Modo hotspot" STR_HOTSPOT_MODE: "Modo hotspot"
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi" STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
STR_OPEN_URL_HINT: "Abra este URL seu navegador" STR_OPEN_URL_HINT: "Abra esta URL no seu navegador"
STR_OR_HTTP_PREFIX: "ou http://" STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:" STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
STR_CALIBRE_WIRELESS: "Calibre sem fio" STR_CALIBRE_WIRELESS: "Calibre sem fio"
@@ -50,29 +50,38 @@ STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "Verificando Wi-Fi..." STR_CHECKING_WIFI: "Verificando Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi" STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
STR_TO_PREFIX: "para" STR_TO_PREFIX: "para"
STR_CALIBRE_RECEIVING: "Recebendo:" STR_CALIBRE_RECEIVING: "Recebendo: "
STR_CALIBRE_RECEIVED: "Recebido:" STR_CALIBRE_RECEIVED: "Recebido: "
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi" STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\"" STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar ao dispositivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\"" STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
STR_CAT_DISPLAY: "Tela" STR_CAT_DISPLAY: "Tela"
STR_CAT_READER: "Leitor" STR_CAT_READER: "Leitor"
STR_CAT_CONTROLS: "Controles" STR_CAT_CONTROLS: "Controles"
STR_CAT_SYSTEM: "Sistema" STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Tela de repouso" STR_SLEEP_SCREEN: "Tela de repouso"
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
STR_SLEEP_COVER_MODE: "Modo capa tela repouso" STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
STR_HIDE_BATTERY: "Ocultar % da bateria" STR_HIDE_BATTERY: "Ocultar % da bateria"
STR_EXTRA_SPACING: "Espaço de parágrafos extra" STR_EXTRA_SPACING: "Espaço de parágrafos extra"
STR_TEXT_AA: "Suavização de texto" STR_TEXT_AA: "Suavização de texto"
STR_TEXT_AA_FULL: "Completa"
STR_TEXT_AA_FAST: "Rápida"
STR_IMAGES: "Imagens"
STR_IMAGES_DISPLAY: "Exibição"
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
STR_IMAGES_SUPPRESS: "Ocultar"
STR_SHORT_PWR_BTN: "Clique curto botão ligar" STR_SHORT_PWR_BTN: "Clique curto botão ligar"
STR_ORIENTATION: "Orientação de leitura" STR_ORIENTATION: "Orientação de leitura"
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais" STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar" STR_LONG_PRESS_BEHAVIOR: "Comportamento de Pressionar e segurar"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado" STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" STR_LONG_PRESS_BEHAVIOR_SKIP: "Pular capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
STR_LONG_PRESS_MENU: "Pressionar e segurar Menu"
STR_FONT_PREVIEW_TEXT: "Um pequeno jabuti xereta viu dez cegonhas felizes. "
STR_FONT_FAMILY: "Fonte do leitor" STR_FONT_FAMILY: "Fonte do leitor"
STR_FONT_SIZE: "Tam. fonte UI" STR_FONT_SIZE: "Tam. fonte UI"
STR_LINE_SPACING: "Espaçamento entre linhas" STR_LINE_SPACING: "Espaçamento entre linhas"
@@ -80,6 +89,7 @@ STR_SCREEN_MARGIN: "Margens da tela"
STR_PARA_ALIGNMENT: "Alinhamento parágrafo" STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
STR_HYPHENATION: "Hifenização" STR_HYPHENATION: "Hifenização"
STR_TIME_TO_SLEEP: "Tempo para repousar" STR_TIME_TO_SLEEP: "Tempo para repousar"
STR_SHOW_HIDDEN_FILES: "Mostrar arquivos ocultos"
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes" STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read" STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read"
STR_REFRESH_FREQ: "Frequência atualização" STR_REFRESH_FREQ: "Frequência atualização"
@@ -127,12 +137,17 @@ STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar" STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso" STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página" STR_PAGE_TURN: "Virar página"
STR_FORCE_REFRESH: "Atualizar tela"
STR_PORTRAIT: "Retrato" STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H" STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido" STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Retrato 180°"
STR_LANDSCAPE_CCW: "Paisagem AH" STR_LANDSCAPE_CCW: "Paisagem AH"
STR_PREV_NEXT: "Ant/Próx" STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant" STR_NEXT_PREV: "Próx/Ant"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Marcador"
STR_DISABLED: "Desabilitado"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Pequeno" STR_SMALL: "Pequeno"
@@ -160,17 +175,21 @@ STR_UPDATING: "Atualizando..."
STR_NO_UPDATE: "Nenhuma atualização disponível" STR_NO_UPDATE: "Nenhuma atualização disponível"
STR_UPDATE_FAILED: "Falha na atualização" STR_UPDATE_FAILED: "Falha na atualização"
STR_UPDATE_COMPLETE: "Atualização concluída" STR_UPDATE_COMPLETE: "Atualização concluída"
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente" STR_POWER_ON_HINT: "Pressione e segure o botão de energia para ligar novamente"
STR_NO_ENTRIES: "Nenhum entries encontrado" STR_RESTARTING_HINT: "Reiniciando... Se não reiniciar, pressione e segure o botão de energia por alguns segundos."
STR_NO_ENTRIES: "Nenhuma entrada encontrada"
STR_DOWNLOADING: "Baixando..." STR_DOWNLOADING: "Baixando..."
STR_DOWNLOAD_FAILED: "Falha no download" STR_DOWNLOAD_FAILED: "Falha no download"
STR_ERROR_MSG: "Erro:" STR_ERROR_MSG: "Erro:"
STR_UNNAMED: "Sem nome" STR_UNNAMED: "Sem nome"
STR_HOLD_OPEN_TO_DELETE: "Segure 'Abrir' para excluir"
STR_NO_SERVER_URL: "Nenhum URL servidor configurado" STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed" STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed" STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
STR_NETWORK_PREFIX: "Rede:" STR_NEXT_PAGE: "Próxima Página »"
STR_IP_ADDRESS_PREFIX: "Endereço IP:" 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_GENERAL_FAILURE: "Erro: falha geral"
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada" STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão" STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
@@ -179,7 +198,9 @@ STR_BACK: "« Voltar"
STR_EXIT: "« Sair" STR_EXIT: "« Sair"
STR_HOME: "« Início" STR_HOME: "« Início"
STR_SELECT: "Escolher" STR_SELECT: "Escolher"
STR_SELECTED: "Selecionado"
STR_TOGGLE: "Alternar" STR_TOGGLE: "Alternar"
STR_TOGGLE_BOOKMARK: "Alternar marcador"
STR_CONFIRM: "Confirmar" STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar" STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar" STR_CONNECT: "Conectar"
@@ -188,6 +209,8 @@ STR_DOWNLOAD: "Baixar"
STR_RETRY: "Tentar novamente" STR_RETRY: "Tentar novamente"
STR_YES: "Sim" STR_YES: "Sim"
STR_NO: "Não" STR_NO: "Não"
STR_SHOW: "Mostrar"
STR_HIDE: "Ocultar"
STR_STATE_ON: "LIG." STR_STATE_ON: "LIG."
STR_STATE_OFF: "DESL." STR_STATE_OFF: "DESL."
STR_NOT_SET: "Não definido" STR_NOT_SET: "Não definido"
@@ -198,15 +221,51 @@ STR_DIR_DOWN: "Baixo"
STR_OK_BUTTON: "OK" STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso" STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
STR_FILTER_CONTRAST: "Contraste" STR_FILTER_CONTRAST: "Contraste"
STR_UI_THEME: "Tema da interface" STR_CUSTOMISE_STATUS_BAR: "Customizar Barra de Status"
STR_CHAPTER_PAGE_COUNT: "Páginas do Capítulo"
STR_BOOK_PROGRESS_PERCENTAGE: "Porcentagem de Progresso"
STR_PROGRESS_BAR: "Barra de Progresso"
STR_PROGRESS_BAR_THICKNESS: "Espessura da Barra de Progresso"
STR_PROGRESS_BAR_THIN: "Fino"
STR_PROGRESS_BAR_MEDIUM: "Médio"
STR_PROGRESS_BAR_THICK: "Grosso"
STR_BOOK: "Livro"
STR_CHAPTER: "Capítulo"
STR_EXAMPLE_CHAPTER: "Capítulo 21"
STR_EXAMPLE_BOOK: "Título do Livro"
STR_PREVIEW: "Prévia"
STR_TITLE: "Título"
STR_BATTERY: "Bateria"
STR_XTC_STATUS_BAR: "Barra de Status XTC"
STR_BOTTOM: "Baixo"
STR_TOP: "Topo"
STR_CLOCK: "Relógio"
STR_CLOCK_UTC_OFFSET: "Offset 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: "Próximo"
STR_CLOCK_SYNC: "Sincronizar Relógio"
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
STR_CLOCK_SYNCING: "Sincronizando 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 conectado"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Conecte ao Wi-Fi primeiro, depois tente novamente."
STR_CLOCK_SYNCED: "Relógio sincronizado"
STR_UI_THEME: "Tema"
STR_THEME_CLASSIC: "Clássico" STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_THEME_LYRA_EXTENDED: "Lyra Estendido"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol" STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais" 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_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Busca"
STR_COVER_CUSTOM: "Capa + personalizado" STR_COVER_CUSTOM: "Capa + personalizado"
STR_QUICK_RESUME: "Retomada rápida" STR_QUICK_RESUME: "Retomada rápida"
STR_MENU_RECENT_BOOKS: "Livros recentes" STR_MENU_RECENT_BOOKS: "Livros recentes"
@@ -220,11 +279,11 @@ STR_CALIBRE_SETUP: "Configuração"
STR_CALIBRE_STATUS: "Status" STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Limpar" STR_CLEAR_BUTTON: "Limpar"
STR_DEFAULT_VALUE: "Padrão" STR_DEFAULT_VALUE: "Padrão"
STR_REMAP_PROMPT: "Pressione um botão frontal cada função" STR_REMAP_PROMPT: "Pressione um botão frontal para cada função"
STR_UNASSIGNED: "Não atribuído" STR_UNASSIGNED: "Não atribuído"
STR_ALREADY_ASSIGNED: "Já atribuído" STR_ALREADY_ASSIGNED: "Já atribuído"
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão" STR_REMAP_RESET_HINT: "Botão lateral de cima: redefinir layout padrão"
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento" STR_REMAP_CANCEL_HINT: "Botão lateral de baixo: cancelar remapeamento"
STR_HW_BACK_LABEL: "Voltar (1º botão)" STR_HW_BACK_LABEL: "Voltar (1º botão)"
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)" STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
STR_HW_LEFT_LABEL: "Esquerda (3º botão)" STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
@@ -234,14 +293,15 @@ STR_GO_HOME_BUTTON: "Ir para o início"
STR_SYNC_PROGRESS: "Sincronizar progresso" STR_SYNC_PROGRESS: "Sincronizar progresso"
STR_DELETE_CACHE: "Excluir cache do livro" STR_DELETE_CACHE: "Excluir cache do livro"
STR_DELETE: "Excluir" STR_DELETE: "Excluir"
STR_CONFIRM_DELETE_BOOKMARK: "Excluir este marcador?"
STR_DISPLAY_QR: "Mostrar página como QR"
STR_CHAPTER_PREFIX: "Capítulo:" STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: "páginas |" STR_PAGES_SEPARATOR: "páginas |"
STR_BOOK_PREFIX: "Livro:" STR_BOOK_PREFIX: "Livro:"
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL" STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
STR_PERCENT_STEP_HINT: "Esq/Dir: 1% Cima/Baixo: 10%"
STR_SYNCING_TIME: "Sincronizando horário..." STR_SYNCING_TIME: "Sincronizando horário..."
STR_CALC_HASH: "Calculando hash documento..." STR_CALC_HASH: "Calculando hash do documento..."
STR_HASH_FAILED: "Falha ao calcular o hash documento" STR_HASH_FAILED: "Falha ao calcular o hash do documento"
STR_FETCH_PROGRESS: "Buscando progresso remoto..." STR_FETCH_PROGRESS: "Buscando progresso remoto..."
STR_UPLOAD_PROGRESS: "Enviando progresso..." STR_UPLOAD_PROGRESS: "Enviando progresso..."
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada" STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
@@ -259,14 +319,69 @@ STR_UPLOAD_PROMPT: "Enviar posição atual?"
STR_UPLOAD_SUCCESS: "Progresso enviado!" STR_UPLOAD_SUCCESS: "Progresso enviado!"
STR_SYNC_FAILED_MSG: "Falha na sincronização" STR_SYNC_FAILED_MSG: "Falha na sincronização"
STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso" STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso"
STR_SECTION_PREFIX: "Seção" STR_SECTION_PREFIX: "Seção "
STR_UPLOAD: "Enviar" STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro" STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido" STR_EMBEDDED_STYLE: "Estilo embutido"
STR_FOCUS_READING: "Leitura focada" STR_FOCUS_READING: "Leitura focada"
STR_OPDS_SERVER_URL: "URL do servidor OPDS" STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido de 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: "[link]"
STR_SCREENSHOT_BUTTON: "Capturar tela" STR_SCREENSHOT_BUTTON: "Capturar tela"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nunca" STR_SLEEP_NEVER: "Nunca"
STR_SLEEP_TIMER_STEP_HINT: "Esq/Dir: 1 min Cima/Baixo: 5 min" 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: "Excluir Servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes"
STR_FONT_BROWSER: "Navegador de fontes"
STR_LOADING_FONT_LIST: "Carregando lista de fontes..."
STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível"
STR_FONT_INSTALLED: "Fonte instalada!"
STR_FONT_INSTALL_FAILED: "Falha na instalação da fonte"
STR_INSTALLED: "Instalada"
STR_DOWNLOAD_ALL: "Baixar tudo"
STR_UPDATE_ALL: "Atualizar tudo"
STR_UPDATE_AVAILABLE: "Atualizar"
STR_CRASH_TITLE: "Falha do sistema"
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi salvo em crash_report.txt. Por favor, inclua este arquivo no seu relatório de erro."
STR_CRASH_REASON: "Motivo da falha:"
STR_CRASH_NO_REASON: "(Nenhum motivo registrado)"
STR_TILT_PAGE_TURN: "Virar página por inclinação" STR_TILT_PAGE_TURN: "Virar página por inclinação"
STR_KB_HINT_MOVE_CURSOR: "Pressione ESQ ou DIR para mover o cursor"
STR_KB_HINT_RETURN_CURSOR: "Pressione ESQ para retornar à posição do cursor"
STR_KB_HINT_HIDE_PASSWORD: "Segure DIR e pressione [***] para ocultar a senha"
STR_KB_HINT_SHOW_PASSWORD: "Segure DIR e pressione [abc] para exibir a senha"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pressione [***] para ocultar a senha"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pressione [abc] para exibir a senha"
STR_KB_HINT_EDIT_ENTRY: "Segure CIMA para editar a entrada"
STR_KB_TIPS: "Dicas:"
STR_KB_HINT_RETURN_KEYBOARD: "Pressione BAIXO para retornar ao teclado"
STR_KB_HINT_EXIT_URL_MODE: "Pressione ABC para sair do modo URL"
STR_KB_HINT_CLEAR_TEXT: "Segure DEL para limpar todo o texto"
STR_KB_HINT_SECONDARY_CHAR: "Segure SELECT para caractere secundário"
STR_KB_HINT_UPPER_SECONDARY: "Segure SELECT para MAIÚSCULO ou caractere secundário"
STR_KB_HINT_LOWER_SECONDARY: "Segure SELECT para minúsculo ou caractere secundário"
STR_KB_HINT_URL_SNIPPETS: "Pressione URL para atalhos"
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
STR_SELECT_FIRMWARE_FILE: "Selecione o arquivo de firmware (.bin)"
STR_NO_BIN_FILES: "Nenhum arquivo .bin encontrado"
STR_VALIDATING_FIRMWARE: "Validando firmware..."
STR_INVALID_FIRMWARE: "Arquivo de firmware inválido"
STR_FIRMWARE_TOO_LARGE: "Firmware grande demais para a partição"
STR_FIRMWARE_TOO_SMALL: "Arquivo de firmware pequeno demais"
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o arquivo"
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 arquivo firmware.bin na raiz do cartão SD e selecione-o"
+9 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Mod ecran de repaus cu copertă"
STR_HIDE_BATTERY: "Ascunde procentul bateriei" STR_HIDE_BATTERY: "Ascunde procentul bateriei"
STR_EXTRA_SPACING: "Spaţiere suplimentară între paragrafe" STR_EXTRA_SPACING: "Spaţiere suplimentară între paragrafe"
STR_TEXT_AA: "Anti-Aliasing text" STR_TEXT_AA: "Anti-Aliasing text"
STR_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Rapid"
STR_IMAGES: "Imagini" STR_IMAGES: "Imagini"
STR_IMAGES_DISPLAY: "Afişare" STR_IMAGES_DISPLAY: "Afişare"
STR_IMAGES_PLACEHOLDER: "Substituent" STR_IMAGES_PLACEHOLDER: "Substituent"
@@ -77,6 +79,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat" STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Sărire capitol" STR_LONG_PRESS_BEHAVIOR_SKIP: "Sărire capitol"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Schimbă orientarea" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Schimbă orientarea"
STR_FONT_PREVIEW_TEXT: "Încă vând gem, whisky bej și tequila roz, preț fix"
STR_FONT_FAMILY: "Familie font lectură" STR_FONT_FAMILY: "Familie font lectură"
STR_FONT_SIZE: "Dimensiune font" STR_FONT_SIZE: "Dimensiune font"
STR_LINE_SPACING: "Spaţiere între rânduri" STR_LINE_SPACING: "Spaţiere între rânduri"
@@ -135,6 +138,7 @@ STR_PAGE_TURN: "Răsfoire pagină"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Orizontal dreapta" STR_LANDSCAPE_CW: "Orizontal dreapta"
STR_INVERTED: "Invers" STR_INVERTED: "Invers"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Orizontal stânga" STR_LANDSCAPE_CCW: "Orizontal stânga"
STR_PREV_NEXT: "Înainte/Înapoi" STR_PREV_NEXT: "Înainte/Înapoi"
STR_NEXT_PREV: "Înapoi/Înainte" STR_NEXT_PREV: "Înapoi/Înainte"
@@ -186,6 +190,9 @@ STR_HOME: "« Acasă"
STR_SELECT: "Selectează" STR_SELECT: "Selectează"
STR_SELECTED: "Selectat" STR_SELECTED: "Selectat"
STR_TOGGLE: "Schimbă" STR_TOGGLE: "Schimbă"
STR_TOGGLE_BOOKMARK: "Comută marcajul"
STR_BOOKMARK_REMOVED: "Marcaj eliminat."
STR_HOLD_OPEN_TO_DELETE: "Țineți apăsat Deschideți pentru a șterge"
STR_CONFIRM: "Confirmă" STR_CONFIRM: "Confirmă"
STR_CANCEL: "Anulare" STR_CANCEL: "Anulare"
STR_CONNECT: "Conectare" STR_CONNECT: "Conectare"
@@ -261,7 +268,6 @@ STR_CHAPTER_PREFIX: "Capitol: "
STR_PAGES_SEPARATOR: " pagini | " STR_PAGES_SEPARATOR: " pagini | "
STR_BOOK_PREFIX: "Carte: " STR_BOOK_PREFIX: "Carte: "
STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL" STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL"
STR_PERCENT_STEP_HINT: "Stânga/Dreapta: 1% Sus/Jos: 10%"
STR_SYNCING_TIME: "Timp de sincronizare..." STR_SYNCING_TIME: "Timp de sincronizare..."
STR_CALC_HASH: "Calcularea hash-ului documentului..." STR_CALC_HASH: "Calcularea hash-ului documentului..."
STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului" STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului"
@@ -293,7 +299,8 @@ STR_NO_FOOTNOTES: "Nicio notă de subsol"
STR_LINK: "[link]" STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată" STR_SLEEP_NEVER: "Niciodată"
STR_SLEEP_TIMER_STEP_HINT: "Stânga/Dreapta: 1 min Sus/Jos: 5 min" STR_STEP_HINT_FRONT: "Butoane frontale:"
STR_STEP_HINT_SIDE: "Butoane laterale:"
STR_SCREENSHOT_BUTTON: "Captură ecran" 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"
+13 -5
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Режим обложки сна"
STR_HIDE_BATTERY: "Скрыть % батареи" STR_HIDE_BATTERY: "Скрыть % батареи"
STR_EXTRA_SPACING: "Доп. интервал абзаца" STR_EXTRA_SPACING: "Доп. интервал абзаца"
STR_TEXT_AA: "Сглаживание текста" STR_TEXT_AA: "Сглаживание текста"
STR_TEXT_AA_FULL: "Полное"
STR_TEXT_AA_FAST: "Быстрое"
STR_IMAGES: "Изображения" STR_IMAGES: "Изображения"
STR_IMAGES_DISPLAY: "Показать" STR_IMAGES_DISPLAY: "Показать"
STR_IMAGES_PLACEHOLDER: "Заглушки" STR_IMAGES_PLACEHOLDER: "Заглушки"
@@ -78,6 +80,8 @@ STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего" STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропуск главы" STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропуск главы"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Изменить ориентацию" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Изменить ориентацию"
STR_LONG_PRESS_MENU: "Долгое нажатие меню"
STR_FONT_PREVIEW_TEXT: "Съешь ещё этих мягких французских булок, да выпей же чаю"
STR_FONT_FAMILY: "Шрифт чтения" STR_FONT_FAMILY: "Шрифт чтения"
STR_FONT_SIZE: "Размер шрифта интерфейса" STR_FONT_SIZE: "Размер шрифта интерфейса"
STR_LINE_SPACING: "Межстрочный интервал" STR_LINE_SPACING: "Межстрочный интервал"
@@ -138,10 +142,13 @@ STR_FORCE_REFRESH: "Обновление экрана"
STR_PORTRAIT: "Портрет" STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Инверсия" STR_INVERTED: "Инверсия"
STR_ORIENTATION_INVERTED: "Портрет 180°"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Вперёд" STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад" STR_NEXT_PREV: "Вперёд/Назад"
STR_DISABLED: "Выключены" STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Закладка"
STR_DISABLED: "Выключено"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Маленький" STR_SMALL: "Маленький"
@@ -176,8 +183,7 @@ STR_DOWNLOADING: "Загрузка..."
STR_DOWNLOAD_FAILED: "Ошибка загрузки" STR_DOWNLOAD_FAILED: "Ошибка загрузки"
STR_ERROR_MSG: "Ошибка:" STR_ERROR_MSG: "Ошибка:"
STR_UNNAMED: "Без имени" STR_UNNAMED: "Без имени"
STR_HOLD_CONFIRM_TO_DELETE: "Удерживайте ОТКРЫТЬ для удаления закладки" STR_HOLD_OPEN_TO_DELETE: "Удерживайте Открыть для удаления"
STR_BOOKMARK_INSTRUCTIONS: "Удерживайте ВЫБРАТЬ для добавления закладки"
STR_NO_SERVER_URL: "URL сервера не настроен" STR_NO_SERVER_URL: "URL сервера не настроен"
STR_FETCH_FEED_FAILED: "Не удалось получить ленту" STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту" STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
@@ -195,6 +201,7 @@ STR_HOME: "« Главная"
STR_SELECT: "Выбрать" STR_SELECT: "Выбрать"
STR_SELECTED: "Выбран" STR_SELECTED: "Выбран"
STR_TOGGLE: "Выбор" STR_TOGGLE: "Выбор"
STR_TOGGLE_BOOKMARK: "Переключить закладку"
STR_CONFIRM: "Подтв." STR_CONFIRM: "Подтв."
STR_CANCEL: "Отмена" STR_CANCEL: "Отмена"
STR_CONNECT: "Подкл." STR_CONNECT: "Подкл."
@@ -257,6 +264,7 @@ STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_BOOKMARKS: "Закладки" STR_BOOKMARKS: "Закладки"
STR_BOOKMARK_ADDED: "Закладка добавлена" STR_BOOKMARK_ADDED: "Закладка добавлена"
STR_BOOKMARK_REMOVED: "Закладка удалена"
STR_OPDS_BROWSER: "OPDS браузер" STR_OPDS_BROWSER: "OPDS браузер"
STR_SEARCH: "Поиск" STR_SEARCH: "Поиск"
STR_COVER_CUSTOM: "Обложка + Свой" STR_COVER_CUSTOM: "Обложка + Свой"
@@ -292,7 +300,6 @@ STR_CHAPTER_PREFIX: "Глава: "
STR_PAGES_SEPARATOR: " стр. | " STR_PAGES_SEPARATOR: " стр. | "
STR_BOOK_PREFIX: "Книга: " STR_BOOK_PREFIX: "Книга: "
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL" STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
STR_SYNCING_TIME: "Синхронизация времени..." STR_SYNCING_TIME: "Синхронизация времени..."
STR_CALC_HASH: "Расчёт хэша документа..." STR_CALC_HASH: "Расчёт хэша документа..."
STR_HASH_FAILED: "Не удалось вычислить хэш документа" STR_HASH_FAILED: "Не удалось вычислить хэш документа"
@@ -326,7 +333,8 @@ STR_LINK: "[ссылка]"
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана" STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин" STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Никогда" STR_SLEEP_NEVER: "Никогда"
STR_SLEEP_TIMER_STEP_HINT: "Влево/Вправо: 1 мин Вверх/Вниз: 5 мин" STR_STEP_HINT_FRONT: "Передние кнопки:"
STR_STEP_HINT_SIDE: "Боковые кнопки:"
STR_ADD_SERVER: "Добавить сервер" STR_ADD_SERVER: "Добавить сервер"
STR_SERVER_NAME: "Имя сервера" STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS" STR_NO_SERVERS: "Нет настроенных серверов OPDS"
+383 -378
View File
@@ -1,378 +1,383 @@
_language_name: "Slovenčina" _language_name: "Slovenčina"
_language_code: "SK" _language_code: "SK"
_order: "24" _order: "24"
STR_CROSSPOINT: "CrossPoint" STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPÚŠŤANIE" STR_BOOTING: "SPÚŠŤANIE"
STR_SLEEPING: "SPÁNOK" STR_SLEEPING: "SPÁNOK"
STR_ENTERING_SLEEP: "Prechod do režimu spánku" STR_ENTERING_SLEEP: "Prechod do režimu spánku"
STR_BROWSE_FILES: "Prehliadať súbory" STR_BROWSE_FILES: "Prehliadať súbory"
STR_FILE_TRANSFER: "Prenos súborov" STR_FILE_TRANSFER: "Prenos súborov"
STR_SETTINGS_TITLE: "Nastavenia" STR_SETTINGS_TITLE: "Nastavenia"
STR_CONTINUE_READING: "Pokračovať v čítaní" STR_CONTINUE_READING: "Pokračovať v čítaní"
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha" STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
STR_START_READING: "Začnite čítať nižšie" STR_START_READING: "Začnite čítať nižšie"
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory" STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
STR_SELECT_CHAPTER: "Vybrať kapitolu" STR_SELECT_CHAPTER: "Vybrať kapitolu"
STR_NO_CHAPTERS: "Žiadne kapitoly" STR_NO_CHAPTERS: "Žiadne kapitoly"
STR_END_OF_BOOK: "Koniec knihy" STR_END_OF_BOOK: "Koniec knihy"
STR_EMPTY_CHAPTER: "Prázdna kapitola" STR_EMPTY_CHAPTER: "Prázdna kapitola"
STR_INDEXING: "Indexovanie" STR_INDEXING: "Indexovanie"
STR_MEMORY_ERROR: "Chyba pamäte" STR_MEMORY_ERROR: "Chyba pamäte"
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky" STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
STR_EMPTY_FILE: "Prázdny súbor" STR_EMPTY_FILE: "Prázdny súbor"
STR_OUT_OF_BOUNDS: "Mimo rozsahu" STR_OUT_OF_BOUNDS: "Mimo rozsahu"
STR_LOADING: "Načítava sa..." STR_LOADING: "Načítava sa..."
STR_LOADING_POPUP: "Načítavanie" STR_LOADING_POPUP: "Načítavanie"
STR_WIFI_NETWORKS: "Wi-Fi siete" STR_WIFI_NETWORKS: "Wi-Fi siete"
STR_NO_NETWORKS: "Nenašli sa žiadne siete" STR_NO_NETWORKS: "Nenašli sa žiadne siete"
STR_NETWORKS_FOUND: "Nájdených %zu sietí" STR_NETWORKS_FOUND: "Nájdených %zu sietí"
STR_SCANNING: "Skenovanie..." STR_SCANNING: "Skenovanie..."
STR_CONNECTING: "Pripájanie..." STR_CONNECTING: "Pripájanie..."
STR_CONNECTED: "Pripojené!" STR_CONNECTED: "Pripojené!"
STR_CONNECTION_FAILED: "Pripojenie zlyhalo" STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
STR_FORGET_NETWORK: "Zabudnúť sieť?" STR_FORGET_NETWORK: "Zabudnúť sieť?"
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?" STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie" STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
STR_JOIN_NETWORK: "Pripojiť sa k sieti" STR_JOIN_NETWORK: "Pripojiť sa k sieti"
STR_CREATE_HOTSPOT: "Vytvoriť hotspot" STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti" STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti"
STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní" STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní"
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..." STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu" STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti" STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti"
STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači" STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači"
STR_OR_HTTP_PREFIX: "alebo http://" STR_OR_HTTP_PREFIX: "alebo http://"
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:" STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
STR_CALIBRE_WIRELESS: "Calibre Wireless" STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené" STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
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: "Zadajte heslo Wi-Fi" STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
STR_TO_PREFIX: "pre" STR_TO_PREFIX: "pre"
STR_CALIBRE_RECEIVING: "Prijímanie:" STR_CALIBRE_RECEIVING: "Prijímanie:"
STR_CALIBRE_RECEIVED: "Prijaté:" STR_CALIBRE_RECEIVED: "Prijaté:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti" STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“" STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“" STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
STR_CAT_DISPLAY: "Displej" STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čítačka" STR_CAT_READER: "Čítačka"
STR_CAT_CONTROLS: "Ovládanie" STR_CAT_CONTROLS: "Ovládanie"
STR_CAT_SYSTEM: "Systém" STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku" STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti" STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
STR_SLEEP_COVER_MODE: "Obrazovka spánku režim krytu" STR_SLEEP_COVER_MODE: "Obrazovka spánku režim krytu"
STR_HIDE_BATTERY: "Skryť % batérie" STR_HIDE_BATTERY: "Skryť % batérie"
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi" STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
STR_TEXT_AA: "Vyhladzovanie textu" STR_TEXT_AA: "Vyhladzovanie textu"
STR_IMAGES: "Obrázky" STR_TEXT_AA_FULL: "Plné"
STR_IMAGES_DISPLAY: "Zobraz" STR_TEXT_AA_FAST: "Rýchle"
STR_IMAGES_PLACEHOLDER: "Rezervované miesto" STR_IMAGES: "Obrázky"
STR_IMAGES_SUPPRESS: "Potlačiť" STR_IMAGES_DISPLAY: "Zobraz"
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania" STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
STR_ORIENTATION: "Orientácia čítania" STR_IMAGES_SUPPRESS: "Potlačiť"
STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)" STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii" STR_ORIENTATION: "Orientácia čítania"
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla" STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu" STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu" STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_FONT_FAMILY: "Rodina písiem čítačky" STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
STR_FONT_SIZE: "Veľkosť písma rozhrania" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
STR_LINE_SPACING: "Riadkovanie čítačky" STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky" STR_FONT_FAMILY: "Rodina písiem čítačky"
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky" STR_FONT_SIZE: "Veľkosť písma rozhrania"
STR_HYPHENATION: "Delenie slov" STR_LINE_SPACING: "Riadkovanie čítačky"
STR_TIME_TO_SLEEP: "Čas do uspania" STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory" STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych" STR_HYPHENATION: "Delenie slov"
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read" STR_TIME_TO_SLEEP: "Čas do uspania"
STR_REFRESH_FREQ: "Frekvencia obnovovania" STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
STR_KOREADER_SYNC: "KOReader Sync" STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
STR_CHECK_UPDATES: "Skontrolovať aktualizácie" STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
STR_LANGUAGE: "Jazyk" STR_REFRESH_FREQ: "Frekvencia obnovovania"
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania" STR_KOREADER_SYNC: "KOReader Sync"
STR_USERNAME: "Používateľské meno" STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
STR_PASSWORD: "Heslo" STR_LANGUAGE: "Jazyk"
STR_SYNC_SERVER_URL: "URL synchronizačného servera" STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
STR_DOCUMENT_MATCHING: "Párovanie dokumentov" STR_USERNAME: "Používateľské meno"
STR_AUTHENTICATE: "Overiť" STR_PASSWORD: "Heslo"
STR_KOREADER_USERNAME: "Používateľské meno KOReader" STR_SYNC_SERVER_URL: "URL synchronizačného servera"
STR_KOREADER_PASSWORD: "Heslo KOReader" STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
STR_FILENAME: "Názov súboru" STR_AUTHENTICATE: "Overiť"
STR_BINARY: "Binárny" STR_KOREADER_USERNAME: "Používateľské meno KOReader"
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje" STR_KOREADER_PASSWORD: "Heslo KOReader"
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo" STR_FILENAME: "Názov súboru"
STR_AUTHENTICATING: "Overovanie..." STR_BINARY: "Binárny"
STR_AUTH_SUCCESS: "Overenie úspešné!" STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
STR_KOREADER_AUTH: "Overenie KOReader" STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie" STR_AUTHENTICATING: "Overovanie..."
STR_AUTH_FAILED: "Overenie zlyhalo" STR_AUTH_SUCCESS: "Overenie úspešné!"
STR_DONE: "Hotovo" STR_KOREADER_AUTH: "Overenie KOReader"
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti." STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!" STR_AUTH_FAILED: "Overenie zlyhalo"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať" STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení." STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..." STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná" STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
STR_ITEMS_REMOVED: "položiek odstránených" STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
STR_FAILED_LOWER: "zlyhalo" STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo" STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe" STR_ITEMS_REMOVED: "položiek odstránených"
STR_DARK: "Tmavý" STR_FAILED_LOWER: "zlyhalo"
STR_LIGHT: "Svetlý" STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
STR_CUSTOM: "Vlastný" STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
STR_COVER: "Obálka" STR_DARK: "Tmavý"
STR_NONE_OPT: "Žiadny" STR_LIGHT: "Svetlý"
STR_FIT: "Prispôsobiť" STR_CUSTOM: "Vlastný"
STR_CROP: "Orezať" STR_COVER: "Obálka"
STR_NEVER: "Nikdy" STR_NONE_OPT: "Žiadny"
STR_IN_READER: "V čítačke" STR_FIT: "Prispôsobiť"
STR_ALWAYS: "Vždy" STR_CROP: "Orezať"
STR_IGNORE: "Ignorovať" STR_NEVER: "Nikdy"
STR_SLEEP: "Spánok" STR_IN_READER: "V čítačke"
STR_PAGE_TURN: "Otáčanie stránok" STR_ALWAYS: "Vždy"
STR_FORCE_REFRESH: "Obnoviť obrazovku" STR_IGNORE: "Ignorovať"
STR_PORTRAIT: "Na výšku" STR_SLEEP: "Spánok"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek" STR_PAGE_TURN: "Otáčanie stránok"
STR_INVERTED: "Obrátený" STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek" STR_PORTRAIT: "Na výšku"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca" STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca" STR_INVERTED: "Invertovaný"
STR_DISABLED: "Vypnuté" STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_NOTO_SERIF: "Noto Serif" STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_NOTO_SANS: "Noto Sans" STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_SMALL: "Malý" STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
STR_MEDIUM: "Stredný" STR_DISABLED: "Vypnuté"
STR_LARGE: "Veľký" STR_NOTO_SERIF: "Noto Serif"
STR_X_LARGE: "Obrovský" STR_NOTO_SANS: "Noto Sans"
STR_TIGHT: "Tesný" STR_SMALL: "Malý"
STR_NORMAL: "Normálny" STR_MEDIUM: "Stredný"
STR_WIDE: "Široký" STR_LARGE: "Veľký"
STR_JUSTIFY: "Zarovnať do bloku" STR_X_LARGE: "Obrovský"
STR_ALIGN_LEFT: "Vľavo" STR_TIGHT: "Tesný"
STR_CENTER: "Na stred" STR_NORMAL: "Normálny"
STR_ALIGN_RIGHT: "Vpravo" STR_WIDE: "Široký"
STR_PAGES_1: "1 strana" STR_JUSTIFY: "Zarovnať do bloku"
STR_PAGES_5: "5 strán" STR_ALIGN_LEFT: "Vľavo"
STR_PAGES_10: "10 strán" STR_CENTER: "Na stred"
STR_PAGES_15: "15 strán" STR_ALIGN_RIGHT: "Vpravo"
STR_PAGES_30: "30 strán" STR_PAGES_1: "1 strana"
STR_UPDATE: "Aktualizácia" STR_PAGES_5: "5 strán"
STR_CHECKING_UPDATE: "Kontrola aktualizácií…" STR_PAGES_10: "10 strán"
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!" STR_PAGES_15: "15 strán"
STR_CURRENT_VERSION: "Aktuálna verzia:" STR_PAGES_30: "30 strán"
STR_NEW_VERSION: "Nová verzia:" STR_UPDATE: "Aktualizácia"
STR_UPDATING: "Aktualizácia..." STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia" STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
STR_UPDATE_FAILED: "Aktualizácia zlyhala" STR_CURRENT_VERSION: "Aktuálna verzia:"
STR_UPDATE_COMPLETE: "Aktualizácia dokončená" STR_NEW_VERSION: "Nová verzia:"
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie" STR_UPDATING: "Aktualizácia..."
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s." STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
STR_NO_ENTRIES: "Neboli nájdené žiadne položky" STR_UPDATE_FAILED: "Aktualizácia zlyhala"
STR_DOWNLOADING: "Sťahovanie..." STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo" STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
STR_ERROR_MSG: "Chyba:" STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
STR_UNNAMED: "Nepomenované" STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
STR_HOLD_CONFIRM_TO_DELETE: "Podrž potvrdiť pre vymazanie" STR_DOWNLOADING: "Sťahovanie..."
STR_BOOKMARK_INSTRUCTIONS: "Podrž tlačidlo Potvrdiť pre vytvorenie záložky." STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera" STR_ERROR_MSG: "Chyba:"
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo" STR_UNNAMED: "Nepomenované"
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo" STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
STR_NEXT_PAGE: "Ďaľsia strana »" STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
STR_PREV_PAGE: "« Predchádzajúca str." STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
STR_NETWORK_PREFIX: "Sieť:" STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
STR_IP_ADDRESS_PREFIX: "IP adresa:" STR_NEXT_PAGE: "Ďaľsia strana »"
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba" STR_PREV_PAGE: "« Predchádzajúca str."
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená" STR_NETWORK_PREFIX: "Sieť:"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia" STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_SD_CARD: "SD karta" STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
STR_BACK: "« Späť" STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
STR_EXIT: "« Koniec" STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
STR_HOME: "« Domov" STR_SD_CARD: "SD karta"
STR_SELECT: "Vybrať" STR_BACK: "« Späť"
STR_SELECTED: "Vybrané" STR_EXIT: "« Koniec"
STR_TOGGLE: "Prepnúť" STR_HOME: "« Domov"
STR_CONFIRM: "Potvrdiť" STR_SELECT: "Vybrať"
STR_CANCEL: "Zrušiť" STR_SELECTED: "Vybrané"
STR_CONNECT: "Pripojiť" STR_TOGGLE: "Prepnúť"
STR_OPEN: "Otvoriť" STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
STR_DOWNLOAD: "Stiahnuť" STR_CONFIRM: "Potvrdiť"
STR_RETRY: "Skúsiť znova" STR_CANCEL: "Zrušiť"
STR_YES: "Áno" STR_CONNECT: "Pripojiť"
STR_NO: "Nie" STR_OPEN: "Otvoriť"
STR_SHOW: "Zobraz" STR_DOWNLOAD: "Stiahnuť"
STR_HIDE: "Skryť" STR_RETRY: "Skúsiť znova"
STR_STATE_ON: "ZAP" STR_YES: "Áno"
STR_STATE_OFF: "VYP" STR_NO: "Nie"
STR_NOT_SET: "Nenastavené" STR_SHOW: "Zobraz"
STR_DIR_LEFT: "Vľavo" STR_HIDE: "Skryť"
STR_DIR_RIGHT: "Vpravo" STR_STATE_ON: "ZAP"
STR_DIR_UP: "Hore" STR_STATE_OFF: "VYP"
STR_DIR_DOWN: "Dole" STR_NOT_SET: "Nenastavené"
STR_OK_BUTTON: "OK" STR_DIR_LEFT: "Vľavo"
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku" STR_DIR_RIGHT: "Vpravo"
STR_FILTER_CONTRAST: "Kontrast" STR_DIR_UP: "Hore"
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar" STR_DIR_DOWN: "Dole"
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly" STR_OK_BUTTON: "OK"
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent" STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
STR_PROGRESS_BAR: "Ukazovateľ čítania" STR_FILTER_CONTRAST: "Kontrast"
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu" STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
STR_PROGRESS_BAR_THIN: "Tenký" STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
STR_PROGRESS_BAR_MEDIUM: "Stredný" STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
STR_PROGRESS_BAR_THICK: "Hrubý" STR_PROGRESS_BAR: "Ukazovateľ čítania"
STR_BOOK: "Kniha" STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
STR_CHAPTER: "Kapitola" STR_PROGRESS_BAR_THIN: "Tenký"
STR_EXAMPLE_CHAPTER: "Kapitola 21" STR_PROGRESS_BAR_MEDIUM: "Stredný"
STR_EXAMPLE_BOOK: "Názov knihy" STR_PROGRESS_BAR_THICK: "Hrubý"
STR_PREVIEW: "Ukážka" STR_BOOK: "Kniha"
STR_TITLE: "Názov" STR_CHAPTER: "Kapitola"
STR_BATTERY: "Batéria" STR_EXAMPLE_CHAPTER: "Kapitola 21"
STR_XTC_STATUS_BAR: "Stavový panel XTC" STR_EXAMPLE_BOOK: "Názov knihy"
STR_BOTTOM: "Dole" STR_PREVIEW: "Ukážka"
STR_TOP: "Hore" STR_TITLE: "Názov"
STR_CLOCK: "Hodiny" STR_BATTERY: "Batéria"
STR_CLOCK_UTC_OFFSET: "UTC posun hodín" STR_XTC_STATUS_BAR: "Stavový panel XTC"
STR_CLOCK_FORMAT: "Formát času" STR_BOTTOM: "Dole"
STR_CLOCK_FORMAT_24H: "24-hodinový" STR_TOP: "Hore"
STR_CLOCK_FORMAT_12H: "12-hodinový" STR_CLOCK: "Hodiny"
STR_CURRENT_TIME: "Aktuálny čas:" STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
STR_NEXT_FIELD: "Ďalej" STR_CLOCK_FORMAT: "Formát času"
STR_CLOCK_SYNC: "Synchronizácia hodín" STR_CLOCK_FORMAT_24H: "24-hodinový"
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz" STR_CLOCK_FORMAT_12H: "12-hodinový"
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..." STR_CURRENT_TIME: "Aktuálny čas:"
STR_CLOCK_SYNC_OK: "Hodiny synchronizované" STR_NEXT_FIELD: "Ďalej"
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala" STR_CLOCK_SYNC: "Synchronizácia hodín"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená" STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova." STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
STR_CLOCK_SYNCED: "Hodiny synchronizované" STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
STR_UI_THEME: "Téma rozhrania" STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
STR_THEME_CLASSIC: "Klasická" STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená"
STR_THEME_LYRA: "Lyra" STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova."
STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_CLOCK_SYNCED: "Hodiny synchronizované"
STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_UI_THEME: "Téma rozhrania"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku" STR_THEME_CLASSIC: "Klasická"
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá" STR_THEME_LYRA: "Lyra"
STR_BOOKMARKS: "Záložky" STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_BOOKMARK_ADDED: "Záložka pridaná." STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_OPDS_BROWSER: "Prehliadač OPDS" STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
STR_SEARCH: "Hľadať" STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
STR_COVER_CUSTOM: "Obálka + Vlastné" STR_BOOKMARKS: "Záložky"
STR_QUICK_RESUME: "Rýchle obnovenie" STR_BOOKMARK_ADDED: "Záložka pridaná."
STR_MENU_RECENT_BOOKS: "Nedávne knihy" STR_BOOKMARK_REMOVED: "Záložka odstránená."
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?" STR_OPDS_BROWSER: "Prehliadač OPDS"
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy" STR_SEARCH: "Hľadať"
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre" STR_COVER_CUSTOM: "Obálka + Vlastné"
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?" STR_QUICK_RESUME: "Rýchle obnovenie"
STR_FORGET_BUTTON: "Zabudnúť" STR_MENU_RECENT_BOOKS: "Nedávne knihy"
STR_CALIBRE_STARTING: "Spúšťanie Calibre..." STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
STR_CALIBRE_SETUP: "Nastavenie" STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
STR_CALIBRE_STATUS: "Stav" STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
STR_CLEAR_BUTTON: "Vymazať" STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
STR_DEFAULT_VALUE: "Predvolené" STR_FORGET_BUTTON: "Zabudnúť"
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu" STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
STR_UNASSIGNED: "Nepriradené" STR_CALIBRE_SETUP: "Nastavenie"
STR_ALREADY_ASSIGNED: "Už priradené" STR_CALIBRE_STATUS: "Stav"
STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie" STR_CLEAR_BUTTON: "Vymazať"
STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie" STR_DEFAULT_VALUE: "Predvolené"
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)" STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)" STR_UNASSIGNED: "Nepriradené"
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)" STR_ALREADY_ASSIGNED: "Už priradené"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)" STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie"
STR_GO_TO_PERCENT: "Prejsť na %" STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie"
STR_GO_HOME_BUTTON: "Prejsť na Domov" STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
STR_SYNC_PROGRESS: "Priebeh synchronizácie" STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy" STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
STR_DELETE: "Vymazať" STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?" STR_GO_TO_PERCENT: "Prejsť na %"
STR_DISPLAY_QR: "Zobraz stránku ako QR" STR_GO_HOME_BUTTON: "Prejsť na Domov"
STR_CHAPTER_PREFIX: "Kapitola:" STR_SYNC_PROGRESS: "Priebeh synchronizácie"
STR_PAGES_SEPARATOR: "strán |" STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
STR_BOOK_PREFIX: "Kniha:" STR_DELETE: "Vymazať"
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy" STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %" STR_DISPLAY_QR: "Zobraz stránku ako QR"
STR_SYNCING_TIME: "Čas synchronizácie..." STR_CHAPTER_PREFIX: "Kapitola:"
STR_CALC_HASH: "Výpočet hashu dokumentu..." STR_PAGES_SEPARATOR: "strán |"
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu" STR_BOOK_PREFIX: "Kniha:"
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..." STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..." STR_SYNCING_TIME: "Čas synchronizácie..."
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené" STR_CALC_HASH: "Výpočet hashu dokumentu..."
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach" STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
STR_PROGRESS_FOUND: "Priebeh nájdený!" STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
STR_REMOTE_LABEL: "Vzdialený:" STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
STR_LOCAL_LABEL: "Lokálny:" STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%" STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%" STR_PROGRESS_FOUND: "Priebeh nájdený!"
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s" STR_REMOTE_LABEL: "Vzdialený:"
STR_APPLY_REMOTE: "Použiť vzdialený priebeh" STR_LOCAL_LABEL: "Lokálny:"
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh" STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh" STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?" STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
STR_UPLOAD_SUCCESS: "Priebeh nahraný!" STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala" STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh" STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
STR_SECTION_PREFIX: "Sekcia" STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
STR_UPLOAD: "Nahrať" STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
STR_BOOK_S_STYLE: "Štýl knihy" STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
STR_EMBEDDED_STYLE: "Vložený štýl" STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
STR_FOCUS_READING: "Sústredené čítanie" STR_SECTION_PREFIX: "Sekcia"
STR_OPDS_SERVER_URL: "URL adresa OPDS servera" STR_UPLOAD: "Nahrať"
STR_SET_SLEEP_COVER: "Nastav obal" STR_BOOK_S_STYLE: "Štýl knihy"
STR_FOOTNOTES: "Poznámky pod čiarou" STR_EMBEDDED_STYLE: "Vložený štýl"
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou" STR_FOCUS_READING: "Sústredené čítanie"
STR_LINK: "[link]" STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky" STR_SET_SLEEP_COVER: "Nastav obal"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_FOOTNOTES: "Poznámky pod čiarou"
STR_SLEEP_NEVER: "Nikdy" STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min" STR_LINK: "[link]"
STR_ADD_SERVER: "Pridať server" STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
STR_SERVER_NAME: "Názov servera" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery" STR_SLEEP_NEVER: "Nikdy"
STR_DELETE_SERVER: "Odstrániť server" STR_STEP_HINT_FRONT: "Predné tlačidlá:"
STR_OPDS_SERVERS: "OPDS servery" STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: " STR_ADD_SERVER: "Pridať server"
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)" STR_SERVER_NAME: "Názov servera"
STR_MANAGE_FONTS: "Správa písiem" STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_FONT_BROWSER: "Prehliadač písiem" STR_DELETE_SERVER: "Odstrániť server"
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..." STR_OPDS_SERVERS: "OPDS servery"
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma" STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_FONT_INSTALLED: "Písmo nainštalované!" STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala" STR_MANAGE_FONTS: "Správa písiem"
STR_INSTALLED: "Nainštalované" STR_FONT_BROWSER: "Prehliadač písiem"
STR_DOWNLOAD_ALL: "Stiahnuť všetko" STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
STR_UPDATE_ALL: "Aktualizovať všetko" STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
STR_UPDATE_AVAILABLE: "Aktualizovať" STR_FONT_INSTALLED: "Písmo nainštalované!"
STR_CRASH_TITLE: "Zlyhanie systému" STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby." STR_INSTALLED: "Nainštalované"
STR_CRASH_REASON: "Dôvod zlyhania:" STR_DOWNLOAD_ALL: "Stiahnuť všetko"
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)" STR_UPDATE_ALL: "Aktualizovať všetko"
STR_TILT_PAGE_TURN: "Otáčanie strán naklonením" STR_UPDATE_AVAILABLE: "Aktualizovať"
STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora" STR_CRASH_TITLE: "Zlyhanie systému"
STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora" STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby."
STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla" STR_CRASH_REASON: "Dôvod zlyhania:"
STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla" STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla" STR_TILT_PAGE_TURN: "Otáčanie strán naklonením"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla" STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora"
STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky" STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora"
STR_KB_TIPS: "Tipy:" STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla"
STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici" STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL" STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla"
STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu" STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak" STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky"
STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak" STR_KB_TIPS: "Tipy:"
STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak" STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici"
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky" STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL"
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty" STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu"
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)" STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak"
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin" STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak"
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..." STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak"
STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru" STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu" STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý" STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?" STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor" STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal" STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!" STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu"
STR_RECOVERY_MODE: "Režim obnovenia" STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho" STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
STR_RECOVERY_MODE: "Režim obnovenia"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
+10 -3
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Način naslovnice v spanju"
STR_HIDE_BATTERY: "Skrij % baterije" STR_HIDE_BATTERY: "Skrij % baterije"
STR_EXTRA_SPACING: "Dodaten razmik med odstavki" STR_EXTRA_SPACING: "Dodaten razmik med odstavki"
STR_TEXT_AA: "Glajenje besedila (AA)" STR_TEXT_AA: "Glajenje besedila (AA)"
STR_TEXT_AA_FULL: "Polno"
STR_TEXT_AA_FAST: "Hitro"
STR_IMAGES: "Slike" STR_IMAGES: "Slike"
STR_IMAGES_DISPLAY: "Prikaži" STR_IMAGES_DISPLAY: "Prikaži"
STR_IMAGES_PLACEHOLDER: "Oznaka mesta" STR_IMAGES_PLACEHOLDER: "Oznaka mesta"
@@ -74,6 +76,7 @@ STR_ORIENTATION: "Orientacija branja"
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov" STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja" STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
STR_FONT_FAMILY: "Pisava bralnika" STR_FONT_FAMILY: "Pisava bralnika"
STR_FONT_SIZE: "Velikost pisave" STR_FONT_SIZE: "Velikost pisave"
STR_LINE_SPACING: "Razmik med vrsticami" STR_LINE_SPACING: "Razmik med vrsticami"
@@ -131,7 +134,8 @@ STR_SLEEP: "Spanje"
STR_PAGE_TURN: "Obračanje strani" STR_PAGE_TURN: "Obračanje strani"
STR_PORTRAIT: "Pokončno" STR_PORTRAIT: "Pokončno"
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)" STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
STR_INVERTED: "Obrnjeno" STR_INVERTED: "Invertirano"
STR_ORIENTATION_INVERTED: "Pokončno 180°"
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)" STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
STR_PREV_NEXT: "Nazaj/Naprej" STR_PREV_NEXT: "Nazaj/Naprej"
STR_NEXT_PREV: "Naprej/Nazaj" STR_NEXT_PREV: "Naprej/Nazaj"
@@ -183,6 +187,9 @@ STR_HOME: "« Domov"
STR_SELECT: "Izberi" STR_SELECT: "Izberi"
STR_SELECTED: "Izbrano" STR_SELECTED: "Izbrano"
STR_TOGGLE: "Preklopi" STR_TOGGLE: "Preklopi"
STR_TOGGLE_BOOKMARK: "Preklopi zaznamek"
STR_BOOKMARK_REMOVED: "Zaznamek odstranjen."
STR_HOLD_OPEN_TO_DELETE: "Držite Odpri za brisanje"
STR_CONFIRM: "Potrdi" STR_CONFIRM: "Potrdi"
STR_CANCEL: "Prekliči" STR_CANCEL: "Prekliči"
STR_CONNECT: "Poveži" STR_CONNECT: "Poveži"
@@ -258,7 +265,6 @@ STR_CHAPTER_PREFIX: "Poglavje: "
STR_PAGES_SEPARATOR: " strani | " STR_PAGES_SEPARATOR: " strani | "
STR_BOOK_PREFIX: "Knjiga: " STR_BOOK_PREFIX: "Knjiga: "
STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju" STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju"
STR_PERCENT_STEP_HINT: "Levo/desno: 1% Gor/dol: 10%"
STR_SYNCING_TIME: "Sinhronizacija časa..." STR_SYNCING_TIME: "Sinhronizacija časa..."
STR_CALC_HASH: "Izračunavanje podpisa dokumenta..." STR_CALC_HASH: "Izračunavanje podpisa dokumenta..."
STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel" STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel"
@@ -290,7 +296,8 @@ STR_NO_FOOTNOTES: "Na tej strani ni opomb"
STR_LINK: "[povezava]" STR_LINK: "[povezava]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikoli" STR_SLEEP_NEVER: "Nikoli"
STR_SLEEP_TIMER_STEP_HINT: "Levo/desno: 1 min Gor/dol: 5 min" STR_STEP_HINT_FRONT: "Sprednji gumbi:"
STR_STEP_HINT_SIDE: "Stranski gumbi:"
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona" 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)"
+14 -5
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Modo de pantalla de suspensión"
STR_HIDE_BATTERY: "Ocultar % de batería" STR_HIDE_BATTERY: "Ocultar % de batería"
STR_EXTRA_SPACING: "Espaciado entre párrafos" STR_EXTRA_SPACING: "Espaciado entre párrafos"
STR_TEXT_AA: "Suavizado de texto" STR_TEXT_AA: "Suavizado de texto"
STR_TEXT_AA_FULL: "Completo"
STR_TEXT_AA_FAST: "Rápido"
STR_IMAGES: "Imágenes" STR_IMAGES: "Imágenes"
STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Reemplazar" STR_IMAGES_PLACEHOLDER: "Reemplazar"
@@ -77,6 +79,8 @@ STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada" STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambiar orient." STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambiar orient."
STR_LONG_PRESS_MENU: "Función de pulsación larga"
STR_FONT_PREVIEW_TEXT: "Benjamín pidió una bebida de kiwi y fresa. Noé, sin vergüenza, la más exquisita champaña del menú"
STR_FONT_FAMILY: "Tipografía" STR_FONT_FAMILY: "Tipografía"
STR_FONT_SIZE: "Tamaño" STR_FONT_SIZE: "Tamaño"
STR_LINE_SPACING: "Interlineado" STR_LINE_SPACING: "Interlineado"
@@ -136,9 +140,12 @@ STR_FORCE_REFRESH: "Refrescar pant."
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horizontal (horario)" STR_LANDSCAPE_CW: "Horizontal (horario)"
STR_INVERTED: "Invertido" STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Al revés"
STR_LANDSCAPE_CCW: "Horizontal (antihorario)" STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
STR_PREV_NEXT: "Ant./Sig." STR_PREV_NEXT: "Ant./Sig."
STR_NEXT_PREV: "Sig./Ant." STR_NEXT_PREV: "Sig./Ant."
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Marcador"
STR_DISABLED: "Desactivados" STR_DISABLED: "Desactivados"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
@@ -174,8 +181,7 @@ STR_DOWNLOADING: "Descargando..."
STR_DOWNLOAD_FAILED: "Fallo de descarga" STR_DOWNLOAD_FAILED: "Fallo de descarga"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Sin nombre" STR_UNNAMED: "Sin nombre"
STR_HOLD_CONFIRM_TO_DELETE: "Mantenga pulsado Confirmar para borrar" STR_HOLD_OPEN_TO_DELETE: "Mantenga pulsado Abrir para borrar"
STR_BOOKMARK_INSTRUCTIONS: "Mantenga pulsado Confirmar en el lector para crear un marcador."
STR_NO_SERVER_URL: "No se configuró URL de servidor" STR_NO_SERVER_URL: "No se configuró URL de servidor"
STR_FETCH_FEED_FAILED: "Fallo al obtener el feed" STR_FETCH_FEED_FAILED: "Fallo al obtener el feed"
STR_PARSE_FEED_FAILED: "Fallo al procesar el feed" STR_PARSE_FEED_FAILED: "Fallo al procesar el feed"
@@ -193,6 +199,7 @@ STR_HOME: "« Inicio"
STR_SELECT: "Selecc." STR_SELECT: "Selecc."
STR_SELECTED: "Seleccionado" STR_SELECTED: "Seleccionado"
STR_TOGGLE: "Cambiar" STR_TOGGLE: "Cambiar"
STR_TOGGLE_BOOKMARK: "Alternar marcador"
STR_CONFIRM: "Confirmar" STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar" STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar" STR_CONNECT: "Conectar"
@@ -256,6 +263,7 @@ STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_BOOKMARKS: "Marcadores" STR_BOOKMARKS: "Marcadores"
STR_BOOKMARK_ADDED: "Marcador añadido." STR_BOOKMARK_ADDED: "Marcador añadido."
STR_BOOKMARK_REMOVED: "Marcador eliminado."
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Buscar" STR_SEARCH: "Buscar"
STR_COVER_CUSTOM: "Portada + Pers." STR_COVER_CUSTOM: "Portada + Pers."
@@ -291,7 +299,6 @@ STR_CHAPTER_PREFIX: "Cap.: "
STR_PAGES_SEPARATOR: " pág. | " STR_PAGES_SEPARATOR: " pág. | "
STR_BOOK_PREFIX: "Libro: " STR_BOOK_PREFIX: "Libro: "
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL" STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
STR_PERCENT_STEP_HINT: "Izq./Dcha.: 1% | Subir/Bajar: 10%"
STR_SYNCING_TIME: "Tiempo de sincronización..." STR_SYNCING_TIME: "Tiempo de sincronización..."
STR_CALC_HASH: "Calculando hash del documento..." STR_CALC_HASH: "Calculando hash del documento..."
STR_HASH_FAILED: "No se pudo calcular el hash del documento" STR_HASH_FAILED: "No se pudo calcular el hash del documento"
@@ -318,13 +325,15 @@ STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado" STR_EMBEDDED_STYLE: "Estilo integrado"
STR_FOCUS_READING: "Lectura enfocada" STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido desde las notas al pie"
STR_SET_SLEEP_COVER: "Pant. sus." STR_SET_SLEEP_COVER: "Pant. sus."
STR_FOOTNOTES: "Pie de página" STR_FOOTNOTES: "Notas al pie"
STR_NO_FOOTNOTES: "No hay notas al pie de esta página" STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
STR_LINK: "[enlace]" STR_LINK: "[enlace]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min." STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Nunca" STR_SLEEP_NEVER: "Nunca"
STR_SLEEP_TIMER_STEP_HINT: "Izq./Dcha.: 1 min. Subir/Bajar: 5 min." STR_STEP_HINT_FRONT: "Botones frontales:"
STR_STEP_HINT_SIDE: "Botones laterales:"
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla" STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
STR_ADD_SERVER: "Agregar servidor" STR_ADD_SERVER: "Agregar servidor"
STR_SERVER_NAME: "Nombre de servidor" STR_SERVER_NAME: "Nombre de servidor"
+9 -4
View File
@@ -66,6 +66,8 @@ 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"
STR_TEXT_AA: "Textkantutjämning" STR_TEXT_AA: "Textkantutjämning"
STR_TEXT_AA_FULL: "Full"
STR_TEXT_AA_FAST: "Snabb"
STR_IMAGES: "Bilder" STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Visa" STR_IMAGES_DISPLAY: "Visa"
STR_IMAGES_PLACEHOLDER: "Platshållare" STR_IMAGES_PLACEHOLDER: "Platshållare"
@@ -78,6 +80,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_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"
STR_LINE_SPACING: "Eboksläsarens linjemellanrum" STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
@@ -137,6 +140,7 @@ STR_FORCE_REFRESH: "Uppdatera skärmen"
STR_PORTRAIT: "Porträtt" STR_PORTRAIT: "Porträtt"
STR_LANDSCAPE_CW: "Landskap medurs" STR_LANDSCAPE_CW: "Landskap medurs"
STR_INVERTED: "Inverterad" STR_INVERTED: "Inverterad"
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"
@@ -175,8 +179,7 @@ STR_DOWNLOADING: "Laddar ner…"
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades" STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
STR_ERROR_MSG: "Fel:" STR_ERROR_MSG: "Fel:"
STR_UNNAMED: "Ej namngiven" STR_UNNAMED: "Ej namngiven"
STR_HOLD_CONFIRM_TO_DELETE: "Håll ned Bekräfta för att radera" STR_HOLD_OPEN_TO_DELETE: "Håll ned Öppna för att radera"
STR_BOOKMARK_INSTRUCTIONS: "Håll Bekräfta i läsaren för att skapa ett bokmärke."
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad" STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde" STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde" STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
@@ -194,6 +197,7 @@ STR_HOME: "« Hem"
STR_SELECT: "Välj " STR_SELECT: "Välj "
STR_SELECTED: "Vald" STR_SELECTED: "Vald"
STR_TOGGLE: "Växla" STR_TOGGLE: "Växla"
STR_TOGGLE_BOOKMARK: "Växla bokmärke"
STR_CONFIRM: "Bekräfta" STR_CONFIRM: "Bekräfta"
STR_CANCEL: "Avbryt" STR_CANCEL: "Avbryt"
STR_CONNECT: "Anslut" STR_CONNECT: "Anslut"
@@ -257,6 +261,7 @@ 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."
STR_BOOKMARK_REMOVED: "Bokmärke borttaget."
STR_OPDS_BROWSER: "OPDS-webbläsare" STR_OPDS_BROWSER: "OPDS-webbläsare"
STR_SEARCH: "Sök" STR_SEARCH: "Sök"
STR_COVER_CUSTOM: "Omslag + Valfri" STR_COVER_CUSTOM: "Omslag + Valfri"
@@ -292,7 +297,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " sidor | " STR_PAGES_SEPARATOR: " sidor | "
STR_BOOK_PREFIX: "Bok:" STR_BOOK_PREFIX: "Bok:"
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress" STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
STR_PERCENT_STEP_HINT: "Vänster/Höger: 1% Upp/Ner 10%"
STR_SYNCING_TIME: "Synkroniserar tid…" STR_SYNCING_TIME: "Synkroniserar tid…"
STR_CALC_HASH: "Beräknar dokumenthash" STR_CALC_HASH: "Beräknar dokumenthash"
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash" STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
@@ -325,7 +329,8 @@ STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
STR_LINK: "[länk]" STR_LINK: "[länk]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig" STR_SLEEP_NEVER: "Aldrig"
STR_SLEEP_TIMER_STEP_HINT: "Vänster/Höger: 1 min Upp/Ner: 5 min" STR_STEP_HINT_FRONT: "Framknappar:"
STR_STEP_HINT_SIDE: "Sidoknappar:"
STR_SCREENSHOT_BUTTON: "Ta en skärmdump" 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"
+10 -3
View File
@@ -64,6 +64,8 @@ STR_SLEEP_COVER_MODE: "Uyku Ekranı Kapak Modu"
STR_HIDE_BATTERY: "Pil Yüzdesini Gizle" STR_HIDE_BATTERY: "Pil Yüzdesini Gizle"
STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu" STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu"
STR_TEXT_AA: "Metin Yumuşatma (AA)" STR_TEXT_AA: "Metin Yumuşatma (AA)"
STR_TEXT_AA_FULL: "Tam"
STR_TEXT_AA_FAST: "Hızlı"
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması" STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü" STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
@@ -72,6 +74,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu" STR_FONT_SIZE: "Arayüz Yazı Boyutu"
STR_LINE_SPACING: "Okuyucu Satır Aralığı" STR_LINE_SPACING: "Okuyucu Satır Aralığı"
@@ -129,7 +132,8 @@ STR_SLEEP: "Uyku"
STR_PAGE_TURN: "Sayfa Çevirme" STR_PAGE_TURN: "Sayfa Çevirme"
STR_PORTRAIT: "Dikey" STR_PORTRAIT: "Dikey"
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)" STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
STR_INVERTED: "Ters" STR_INVERTED: "Negatif"
STR_ORIENTATION_INVERTED: "Dikey 180°"
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)" STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
STR_PREV_NEXT: "Önceki/Sonraki" STR_PREV_NEXT: "Önceki/Sonraki"
STR_NEXT_PREV: "Sonraki/Önceki" STR_NEXT_PREV: "Sonraki/Önceki"
@@ -180,6 +184,9 @@ STR_EXIT: "« Çıkış"
STR_HOME: "« Ana Sayfa" STR_HOME: "« Ana Sayfa"
STR_SELECT: "Seç" STR_SELECT: "Seç"
STR_TOGGLE: "Değiştir" STR_TOGGLE: "Değiştir"
STR_TOGGLE_BOOKMARK: "Yer imini değiştir"
STR_BOOKMARK_REMOVED: "Yer imi kaldırıldı."
STR_HOLD_OPEN_TO_DELETE: "Silmek için Aç düğmesini basılı tutun"
STR_CONFIRM: "Onayla" STR_CONFIRM: "Onayla"
STR_CANCEL: "İptal" STR_CANCEL: "İptal"
STR_CONNECT: "Bağlan" STR_CONNECT: "Bağlan"
@@ -236,7 +243,6 @@ STR_CHAPTER_PREFIX: "Bölüm: "
STR_PAGES_SEPARATOR: " sayfa | " STR_PAGES_SEPARATOR: " sayfa | "
STR_BOOK_PREFIX: "Kitap: " STR_BOOK_PREFIX: "Kitap: "
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin" STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
STR_PERCENT_STEP_HINT: "Sol/Sağ: %1 Yukarı/Aşağı: %10"
STR_SYNCING_TIME: "Zaman senkronize ediliyor..." STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
STR_CALC_HASH: "Belge özeti hesaplanıyor..." STR_CALC_HASH: "Belge özeti hesaplanıyor..."
STR_HASH_FAILED: "Belge özeti hesaplanamadı" STR_HASH_FAILED: "Belge özeti hesaplanamadı"
@@ -284,7 +290,8 @@ STR_IMAGES_SUPPRESS: "Bastır"
STR_LINK: "[bağlantı]" STR_LINK: "[bağlantı]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak" STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla" STR_SLEEP_NEVER: "Asla"
STR_SLEEP_TIMER_STEP_HINT: "Sol/Sağ: 1 dak Yukarı/Aşağı: 5 dak" STR_STEP_HINT_FRONT: "Ön tuşlar:"
STR_STEP_HINT_SIDE: "Yan tuşlar:"
STR_NO_FILES_FOUND: "Dosya bulunamadı" STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok" STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme" STR_PREVIEW: "Önizleme"
+35 -8
View File
@@ -61,10 +61,13 @@ STR_CAT_READER: "Читач"
STR_CAT_CONTROLS: "Кнопки" STR_CAT_CONTROLS: "Кнопки"
STR_CAT_SYSTEM: "Система" STR_CAT_SYSTEM: "Система"
STR_SLEEP_SCREEN: "Екран у режимі сну" STR_SLEEP_SCREEN: "Екран у режимі сну"
STR_QUICK_RESUME_TIMEOUT: "Швидке поверн. після таймауту"
STR_SLEEP_COVER_MODE: "Режим заповнення" STR_SLEEP_COVER_MODE: "Режим заповнення"
STR_HIDE_BATTERY: "Приховати % батареї" STR_HIDE_BATTERY: "Приховати % батареї"
STR_EXTRA_SPACING: "Додатковий інтервал між абзацами" STR_EXTRA_SPACING: "Додатковий інтервал між абзацами"
STR_TEXT_AA: "Згладжування тексту" STR_TEXT_AA: "Згладжування тексту"
STR_TEXT_AA_FULL: "Повне"
STR_TEXT_AA_FAST: "Швидке"
STR_IMAGES: "Зображення" STR_IMAGES: "Зображення"
STR_IMAGES_DISPLAY: "Показати" STR_IMAGES_DISPLAY: "Показати"
STR_IMAGES_PLACEHOLDER: "Заглушка" STR_IMAGES_PLACEHOLDER: "Заглушка"
@@ -77,6 +80,7 @@ STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настик
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає" STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Наступ. розділ (утримув.)" STR_LONG_PRESS_BEHAVIOR_SKIP: "Наступ. розділ (утримув.)"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Зміна орієнтації екрану" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Зміна орієнтації екрану"
STR_FONT_PREVIEW_TEXT: "Єхидна, ґава, їжак ще й шиплячі плазуни бігцем форсують Янцзи"
STR_FONT_FAMILY: "Шрифт" STR_FONT_FAMILY: "Шрифт"
STR_FONT_SIZE: "Розмір шрифту" STR_FONT_SIZE: "Розмір шрифту"
STR_LINE_SPACING: "Міжрядковий інтервал" STR_LINE_SPACING: "Міжрядковий інтервал"
@@ -85,8 +89,8 @@ STR_PARA_ALIGNMENT: "Вирівнювання тексту"
STR_HYPHENATION: "Перенесення слів" STR_HYPHENATION: "Перенесення слів"
STR_TIME_TO_SLEEP: "Перехід в режим сну" STR_TIME_TO_SLEEP: "Перехід в режим сну"
STR_SHOW_HIDDEN_FILES: "Показати приховані файли" STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
STR_REMOVE_READ_FROM_RECENTS: "Очищати прочитані книги зі списку останніх" STR_REMOVE_READ_FROM_RECENTS: "Приховувати прочитані книги"
STR_MOVE_FINISHED_TO_READ: "Переміщати прочитані книги до теки Read" STR_MOVE_FINISHED_TO_READ: "Переміщати прочит. в теку Read"
STR_REFRESH_FREQ: "Частота оновлення екрану" STR_REFRESH_FREQ: "Частота оновлення екрану"
STR_KOREADER_SYNC: "Синхронізація KOReader" STR_KOREADER_SYNC: "Синхронізація KOReader"
STR_CHECK_UPDATES: "Перевірити оновлення системи" STR_CHECK_UPDATES: "Перевірити оновлення системи"
@@ -135,10 +139,12 @@ STR_PAGE_TURN: "Наст. сторінка"
STR_FORCE_REFRESH: "Оновити екран" STR_FORCE_REFRESH: "Оновити екран"
STR_PORTRAIT: "Книжкова" STR_PORTRAIT: "Книжкова"
STR_LANDSCAPE_CW: "Альбом. за год." STR_LANDSCAPE_CW: "Альбом. за год."
STR_INVERTED: "Перевернутий" STR_INVERTED: "Інверсія"
STR_ORIENTATION_INVERTED: "Книжкова 180°"
STR_LANDSCAPE_CCW: "Альбом. проти год." STR_LANDSCAPE_CCW: "Альбом. проти год."
STR_PREV_NEXT: "Попер/Наст" STR_PREV_NEXT: "Попер/Наст"
STR_NEXT_PREV: "Наст/Попер" STR_NEXT_PREV: "Наст/Попер"
STR_DISABLED: "Вимкнуто"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Малий" STR_SMALL: "Малий"
@@ -173,6 +179,7 @@ STR_DOWNLOADING: "Завантаження..."
STR_DOWNLOAD_FAILED: "Завантаження не вдалося" STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
STR_ERROR_MSG: "Помилка:" STR_ERROR_MSG: "Помилка:"
STR_UNNAMED: "Без назви" STR_UNNAMED: "Без назви"
STR_HOLD_OPEN_TO_DELETE: "Утримуйте Відкрити, щоб видалити"
STR_NO_SERVER_URL: "URL сервера не налаштовано" STR_NO_SERVER_URL: "URL сервера не налаштовано"
STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку" STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку"
STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку" STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку"
@@ -190,6 +197,7 @@ STR_HOME: "« Додому"
STR_SELECT: "Вибрати" STR_SELECT: "Вибрати"
STR_SELECTED: "Вибрано" STR_SELECTED: "Вибрано"
STR_TOGGLE: "Обрати" STR_TOGGLE: "Обрати"
STR_TOGGLE_BOOKMARK: "Перемкнути закладку"
STR_CONFIRM: "Підтвердити" STR_CONFIRM: "Підтвердити"
STR_CANCEL: "Скасувати" STR_CANCEL: "Скасувати"
STR_CONNECT: "Приєдн." STR_CONNECT: "Приєдн."
@@ -228,18 +236,35 @@ STR_BATTERY: "Акумулятор"
STR_XTC_STATUS_BAR: "XTC Рядок прогресу" STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
STR_BOTTOM: "Низ" STR_BOTTOM: "Низ"
STR_TOP: "Верх" STR_TOP: "Верх"
STR_CLOCK: "Годинник"
STR_CLOCK_UTC_OFFSET: "Часовий пояс"
STR_CLOCK_FORMAT: "Формат Годинника"
STR_CLOCK_FORMAT_24H: "24-години"
STR_CLOCK_FORMAT_12H: "12-годин"
STR_CURRENT_TIME: "Поточний час:"
STR_NEXT_FIELD: "наступний"
STR_CLOCK_SYNC: "Синхр. Годинник"
STR_CLOCK_SYNC_NOW: "Синхр. годин. зараз"
STR_CLOCK_SYNCING: "Синхр. через NTP..."
STR_CLOCK_SYNC_OK: "Успішна синхр."
STR_CLOCK_SYNC_FAIL: "Невдала синхр. "
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi не під'єднано"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Під'єднайтесь спершу до Wi-Fi, тоді спробуйте знову."
STR_CLOCK_SYNCED: "Годинник синхр."
STR_UI_THEME: "Тема інтерфейсу" STR_UI_THEME: "Тема інтерфейсу"
STR_THEME_CLASSIC: "Класична" STR_THEME_CLASSIC: "Класична"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці" STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці"
STR_QUICK_RESUME_TIMEOUT: "Швидке продовження після таймауту"
STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки" STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки"
STR_BOOKMARKS: "Закладки"
STR_BOOKMARK_ADDED: "Закладку додано"
STR_BOOKMARK_REMOVED: "Закладку видалено"
STR_OPDS_BROWSER: "Браузер OPDS" STR_OPDS_BROWSER: "Браузер OPDS"
STR_SEARCH: "Пошук" STR_SEARCH: "Пошук"
STR_COVER_CUSTOM: "Обкл. + власне" STR_COVER_CUSTOM: "Обкл. + власне"
STR_QUICK_RESUME: "Швидке продовження" STR_QUICK_RESUME: "Швидке поверн."
STR_MENU_RECENT_BOOKS: "Останні книги" STR_MENU_RECENT_BOOKS: "Останні книги"
STR_REMOVE_FROM_RECENTS: "Видалити з останніх книг?" STR_REMOVE_FROM_RECENTS: "Видалити з останніх книг?"
STR_NO_RECENT_BOOKS: "Немає останніх книг" STR_NO_RECENT_BOOKS: "Немає останніх книг"
@@ -265,12 +290,12 @@ STR_GO_HOME_BUTTON: "На головну"
STR_SYNC_PROGRESS: "Прогрес синхронізації" STR_SYNC_PROGRESS: "Прогрес синхронізації"
STR_DELETE_CACHE: "Видалити кеш книги" STR_DELETE_CACHE: "Видалити кеш книги"
STR_DELETE: "Видалити" STR_DELETE: "Видалити"
STR_CONFIRM_DELETE_BOOKMARK: "Видалити цю закладку?"
STR_DISPLAY_QR: "Показати сторінку як QR-код" STR_DISPLAY_QR: "Показати сторінку як QR-код"
STR_CHAPTER_PREFIX: "Розділ: " STR_CHAPTER_PREFIX: "Розділ: "
STR_PAGES_SEPARATOR: " сторінок | " STR_PAGES_SEPARATOR: " сторінок | "
STR_BOOK_PREFIX: "Книга: " STR_BOOK_PREFIX: "Книга: "
STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL" STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL"
STR_PERCENT_STEP_HINT: "Вліво/Вправо: 1% Вгору/Вниз: 10%"
STR_SYNCING_TIME: "Синхронізація часу..." STR_SYNCING_TIME: "Синхронізація часу..."
STR_CALC_HASH: "Обчислення хешу документа..." STR_CALC_HASH: "Обчислення хешу документа..."
STR_HASH_FAILED: "Не вдалося обчислити хеш документа" STR_HASH_FAILED: "Не вдалося обчислити хеш документа"
@@ -297,14 +322,16 @@ STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Вбудований стиль" STR_EMBEDDED_STYLE: "Вбудований стиль"
STR_FOCUS_READING: "Фокусне читання" STR_FOCUS_READING: "Фокусне читання"
STR_OPDS_SERVER_URL: "URL сервера OPDS" STR_OPDS_SERVER_URL: "URL сервера OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Швидке повернення з приміток"
STR_SET_SLEEP_COVER: "Як обкл." STR_SET_SLEEP_COVER: "Як обкл."
STR_FOOTNOTES: "Примітки" STR_FOOTNOTES: "Примітки"
STR_NO_FOOTNOTES: "На цій сторінці немає приміток" STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
STR_LINK: "[посилання]" STR_LINK: "[посилання]"
STR_SCREENSHOT_BUTTON: "Знімок екрана"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв" STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколи" STR_SLEEP_NEVER: "Ніколи"
STR_SLEEP_TIMER_STEP_HINT: "Вліво/Вправо: 1 хв Вгору/Вниз: 5 хв" STR_STEP_HINT_FRONT: "Передні кнопки:"
STR_SCREENSHOT_BUTTON: "Знімок екрана" STR_STEP_HINT_SIDE: "Бічні кнопки:"
STR_ADD_SERVER: "Додати сервер" STR_ADD_SERVER: "Додати сервер"
STR_SERVER_NAME: "Назва сервера" STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS" STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
+13 -4
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Mode de pantalla de 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_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Ràpid"
STR_IMAGES: "Imatges" STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_PLACEHOLDER: "Text de mostra"
@@ -78,6 +80,8 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el 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_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: "Grandària de la lletra (UI)"
STR_LINE_SPACING: "Interlineat del lector" STR_LINE_SPACING: "Interlineat del lector"
@@ -87,7 +91,8 @@ STR_HYPHENATION: "Partició de mots"
STR_TIME_TO_SLEEP: "Temps per entrar en repòs" STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Mai" STR_SLEEP_NEVER: "Mai"
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min" STR_STEP_HINT_FRONT: "Botons frontals:"
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"
@@ -139,9 +144,12 @@ STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Punt de llibre"
STR_DISABLED: "Desactivats" STR_DISABLED: "Desactivats"
STR_NOTO_SERIF: "Noto Serif" STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans" STR_NOTO_SANS: "Noto Sans"
@@ -176,8 +184,7 @@ 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_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar" STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre."
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 feed"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
@@ -193,6 +200,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: "Canvia punt de llibre"
STR_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
STR_CONNECT: "Connecta" STR_CONNECT: "Connecta"
@@ -236,6 +244,7 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
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."
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS" 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"
@@ -270,7 +279,6 @@ 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, afegiu /opds a la URL"
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
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 el hash del document..."
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document" STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
@@ -297,6 +305,7 @@ STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat" STR_EMBEDDED_STYLE: "Estil incrustat"
STR_FOCUS_READING: "Lectura enfocada" STR_FOCUS_READING: "Lectura enfocada"
STR_OPDS_SERVER_URL: "URL del servidor OPDS" STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorn ràpid des de les notes al peu"
STR_FOOTNOTES: "Notes al peu" 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ç]"
+10 -5
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Kiểu ảnh bìa màn hình ngủ"
STR_HIDE_BATTERY: "Ẩn % pin" STR_HIDE_BATTERY: "Ẩn % pin"
STR_EXTRA_SPACING: "Giãn cách đoạn thêm" STR_EXTRA_SPACING: "Giãn cách đoạn thêm"
STR_TEXT_AA: "Khử răng cưa chữ" STR_TEXT_AA: "Khử răng cưa chữ"
STR_TEXT_AA_FULL: "Đầy đủ"
STR_TEXT_AA_FAST: "Nhanh"
STR_IMAGES: "Hình ảnh" STR_IMAGES: "Hình ảnh"
STR_IMAGES_DISPLAY: "Hiển thị" STR_IMAGES_DISPLAY: "Hiển thị"
STR_IMAGES_PLACEHOLDER: "Khung thay thế" STR_IMAGES_PLACEHOLDER: "Khung thay thế"
@@ -78,6 +80,7 @@ STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ nút"
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT" STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương" STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng"
STR_FONT_PREVIEW_TEXT: "Trường quê em do bố của em xây kĩ nên sạch và đẹp lắm"
STR_FONT_FAMILY: "Phông chữ trình đọc" STR_FONT_FAMILY: "Phông chữ trình đọc"
STR_FONT_SIZE: "Cỡ chữ trình đọc" STR_FONT_SIZE: "Cỡ chữ trình đọc"
STR_LINE_SPACING: "Giãn dòng trình đọc" STR_LINE_SPACING: "Giãn dòng trình đọc"
@@ -136,7 +139,8 @@ STR_PAGE_TURN: "Lật trang"
STR_FORCE_REFRESH: "Làm tươi màn hình" STR_FORCE_REFRESH: "Làm tươi màn hình"
STR_PORTRAIT: "Dọc" STR_PORTRAIT: "Dọc"
STR_LANDSCAPE_CW: "Ngang (thuận)" STR_LANDSCAPE_CW: "Ngang (thuận)"
STR_INVERTED: "Lật ngược" STR_INVERTED: "Đảo màu"
STR_ORIENTATION_INVERTED: "Dọc 180°"
STR_LANDSCAPE_CCW: "Ngang (ngược)" STR_LANDSCAPE_CCW: "Ngang (ngược)"
STR_PREV_NEXT: "Trước/Sau" STR_PREV_NEXT: "Trước/Sau"
STR_NEXT_PREV: "Sau/Trước" STR_NEXT_PREV: "Sau/Trước"
@@ -175,8 +179,7 @@ STR_DOWNLOADING: "Đang tải về..."
STR_DOWNLOAD_FAILED: "Tải về thất bại" STR_DOWNLOAD_FAILED: "Tải về thất bại"
STR_ERROR_MSG: "Lỗi:" STR_ERROR_MSG: "Lỗi:"
STR_UNNAMED: "Không tên" STR_UNNAMED: "Không tên"
STR_HOLD_CONFIRM_TO_DELETE: "Giữ Xác nhận để xóa" STR_HOLD_OPEN_TO_DELETE: "Giữ Mở để xóa"
STR_BOOKMARK_INSTRUCTIONS: "Giữ Xác nhận trong trình đọc để tạo dấu trang."
STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ" STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ"
STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp" STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp"
STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp" STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp"
@@ -194,6 +197,7 @@ STR_HOME: "« Thư viện"
STR_SELECT: "Chọn" STR_SELECT: "Chọn"
STR_SELECTED: "Đã chọn" STR_SELECTED: "Đã chọn"
STR_TOGGLE: "Bật/Tắt" STR_TOGGLE: "Bật/Tắt"
STR_TOGGLE_BOOKMARK: "Bật/tắt dấu trang"
STR_CONFIRM: "Xác nhận" STR_CONFIRM: "Xác nhận"
STR_CANCEL: "Hủy" STR_CANCEL: "Hủy"
STR_CONNECT: "Kết nối" STR_CONNECT: "Kết nối"
@@ -256,6 +260,7 @@ STR_SUNLIGHT_FADING_FIX: "Khắc phục mờ dưới nắng"
STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước" STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước"
STR_BOOKMARKS: "Dấu trang" STR_BOOKMARKS: "Dấu trang"
STR_BOOKMARK_ADDED: "Đã thêm dấu trang." STR_BOOKMARK_ADDED: "Đã thêm dấu trang."
STR_BOOKMARK_REMOVED: "Đã xóa dấu trang."
STR_OPDS_BROWSER: "Trình duyệt OPDS" STR_OPDS_BROWSER: "Trình duyệt OPDS"
STR_SEARCH: "Tìm kiếm" STR_SEARCH: "Tìm kiếm"
STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh" STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh"
@@ -291,7 +296,6 @@ STR_CHAPTER_PREFIX: "Chương: "
STR_PAGES_SEPARATOR: " trang | " STR_PAGES_SEPARATOR: " trang | "
STR_BOOK_PREFIX: "Sách: " STR_BOOK_PREFIX: "Sách: "
STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL" STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL"
STR_PERCENT_STEP_HINT: "Trái/Phải: 1% Lên/Xuống: 10%"
STR_SYNCING_TIME: "Đang đồng bộ giờ..." STR_SYNCING_TIME: "Đang đồng bộ giờ..."
STR_CALC_HASH: "Đang tính mã băm tài liệu..." STR_CALC_HASH: "Đang tính mã băm tài liệu..."
STR_HASH_FAILED: "Không tính được mã băm tài liệu" STR_HASH_FAILED: "Không tính được mã băm tài liệu"
@@ -325,7 +329,8 @@ STR_LINK: "[liên kết]"
STR_SCREENSHOT_BUTTON: "Chụp màn hình" STR_SCREENSHOT_BUTTON: "Chụp màn hình"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút" STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút"
STR_SLEEP_NEVER: "Không bao giờ" STR_SLEEP_NEVER: "Không bao giờ"
STR_SLEEP_TIMER_STEP_HINT: "Trái/Phải: 1 phút Lên/Xuống: 5 phút" STR_STEP_HINT_FRONT: "Nút trước:"
STR_STEP_HINT_SIDE: "Nút bên:"
STR_ADD_SERVER: "Thêm máy chủ" STR_ADD_SERVER: "Thêm máy chủ"
STR_SERVER_NAME: "Tên máy chủ" STR_SERVER_NAME: "Tên máy chủ"
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS" STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
+122 -2
View File
@@ -164,6 +164,7 @@ namespace {
constexpr int MAX_MCU_HEIGHT = 16; constexpr int MAX_MCU_HEIGHT = 16;
constexpr size_t JPEG_DECODER_SIZE = 20 * 1024; constexpr size_t JPEG_DECODER_SIZE = 20 * 1024;
constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024; constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024;
constexpr uint32_t FP_ONE = 1UL << 16;
// Static file pointer for JPEGDEC open callback. // Static file pointer for JPEGDEC open callback.
// Safe in single-threaded embedded context; never accessed concurrently. // Safe in single-threaded embedded context; never accessed concurrently.
@@ -208,6 +209,9 @@ struct BmpConvertCtx {
bool needsScaling; bool needsScaling;
uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point
uint32_t scaleY_fp; uint32_t scaleY_fp;
bool smoothUpscale;
uint32_t smoothScaleX_fp;
uint32_t smoothScaleY_fp;
// Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels) // Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels)
// Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row // Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row
@@ -219,6 +223,13 @@ struct BmpConvertCtx {
std::unique_ptr<uint32_t[]> rowAccum; std::unique_ptr<uint32_t[]> rowAccum;
std::unique_ptr<uint32_t[]> rowCount; std::unique_ptr<uint32_t[]> rowCount;
int smoothNextOutY;
int smoothPrevY;
std::unique_ptr<uint8_t[]> smoothRows;
uint8_t* smoothPrevRow;
uint8_t* smoothCurrRow;
uint8_t* smoothOutRow;
std::unique_ptr<uint8_t[]> bmpRow; std::unique_ptr<uint8_t[]> bmpRow;
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer; std::unique_ptr<AtkinsonDitherer> atkinsonDitherer;
@@ -265,6 +276,88 @@ static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY)
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow); ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
} }
// Matches the progressive-JPEG smoothing used by JpegToFramebufferConverter, but stays
// local because cover generation streams dithered BMP rows instead of framebuffer pixels.
static uint32_t interpolationStep(const int srcSize, const int outSize) {
if (srcSize <= 1 || outSize <= 1) return 0;
return (static_cast<uint32_t>(srcSize - 1) << 16) / static_cast<uint32_t>(outSize - 1);
}
static uint32_t interpolatedSourceFp(const int outIndex, const int outSize, const int srcSize, const uint32_t step) {
if (srcSize <= 1 || outSize <= 1) return 0;
if (outIndex >= outSize - 1) return static_cast<uint32_t>(srcSize - 1) << 16;
return static_cast<uint32_t>(outIndex) * step;
}
static void scaleRowLinear(BmpConvertCtx* ctx, const uint8_t* srcRow, uint8_t* dstRow) {
for (int outX = 0; outX < ctx->outWidth; outX++) {
const uint32_t srcX_fp = interpolatedSourceFp(outX, ctx->outWidth, ctx->srcWidth, ctx->smoothScaleX_fp);
const int x0 = srcX_fp >> 16;
const int x1 = (x0 + 1 < ctx->srcWidth) ? (x0 + 1) : x0;
const uint32_t fx = srcX_fp & (FP_ONE - 1);
dstRow[outX] = static_cast<uint8_t>((srcRow[x0] * (FP_ONE - fx) + srcRow[x1] * fx) >> 16);
}
}
static void writeBlendedRow(BmpConvertCtx* ctx, const uint8_t* row0, const uint8_t* row1, const uint32_t fy,
const int outY) {
const uint32_t invFy = FP_ONE - fy;
for (int outX = 0; outX < ctx->outWidth; outX++) {
ctx->smoothOutRow[outX] = static_cast<uint8_t>((row0[outX] * invFy + row1[outX] * fy) >> 16);
}
writeOutputRow(ctx, ctx->smoothOutRow, outY);
}
static void processSmoothSourceRow(BmpConvertCtx* ctx, const uint8_t* srcRow, const int srcY) {
scaleRowLinear(ctx, srcRow, ctx->smoothCurrRow);
if (ctx->smoothPrevY < 0) {
uint8_t* tmp = ctx->smoothPrevRow;
ctx->smoothPrevRow = ctx->smoothCurrRow;
ctx->smoothCurrRow = tmp;
ctx->smoothPrevY = srcY;
if (ctx->srcHeight <= 1) {
while (ctx->smoothNextOutY < ctx->outHeight) {
writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY);
ctx->smoothNextOutY++;
}
return;
}
return;
}
while (ctx->smoothNextOutY < ctx->outHeight) {
const uint32_t srcY_fp =
interpolatedSourceFp(ctx->smoothNextOutY, ctx->outHeight, ctx->srcHeight, ctx->smoothScaleY_fp);
const int y0 = srcY_fp >> 16;
const int y1 = (y0 + 1 < ctx->srcHeight) ? (y0 + 1) : y0;
if (y1 > srcY) break;
const uint8_t* row0 = (y0 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow;
const uint8_t* row1 = (y1 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow;
writeBlendedRow(ctx, row0, row1, srcY_fp & (FP_ONE - 1), ctx->smoothNextOutY);
ctx->smoothNextOutY++;
}
uint8_t* tmp = ctx->smoothPrevRow;
ctx->smoothPrevRow = ctx->smoothCurrRow;
ctx->smoothCurrRow = tmp;
ctx->smoothPrevY = srcY;
}
static void finishSmoothUpscale(BmpConvertCtx* ctx) {
if (ctx->smoothPrevY < 0) {
LOG_ERR("JPG", "No progressive rows decoded for smoothing");
ctx->error = true;
return;
}
while (ctx->smoothNextOutY < ctx->outHeight) {
writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY);
ctx->smoothNextOutY++;
}
}
// Flush one scaled output row from Y-axis accumulators and advance currentOutY // Flush one scaled output row from Y-axis accumulators and advance currentOutY
static void flushScaledRow(BmpConvertCtx* ctx) { static void flushScaledRow(BmpConvertCtx* ctx) {
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow); memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
@@ -344,7 +437,9 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) { for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) {
const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth; const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth;
if (!ctx->needsScaling) { if (ctx->smoothUpscale) {
processSmoothSourceRow(ctx, srcRow, y);
} else if (!ctx->needsScaling) {
// 1:1 — outWidth == srcWidth, write directly // 1:1 — outWidth == srcWidth, write directly
writeOutputRow(ctx, srcRow, y); writeOutputRow(ctx, srcRow, y);
} else { } else {
@@ -472,6 +567,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
needsScaling = true; needsScaling = true;
} }
const bool smoothUpscale =
progressiveDecode && needsScaling && scaleSrcWidth <= outWidth && scaleSrcHeight <= outHeight;
// Write BMP header with output dimensions // Write BMP header with output dimensions
int bytesPerRow; int bytesPerRow;
if (USE_8BIT_OUTPUT && !oneBit) { if (USE_8BIT_OUTPUT && !oneBit) {
@@ -496,6 +594,11 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
ctx.needsScaling = needsScaling; ctx.needsScaling = needsScaling;
ctx.scaleX_fp = scaleX_fp; ctx.scaleX_fp = scaleX_fp;
ctx.scaleY_fp = scaleY_fp; ctx.scaleY_fp = scaleY_fp;
ctx.smoothUpscale = smoothUpscale;
ctx.smoothScaleX_fp = interpolationStep(ctx.srcWidth, outWidth);
ctx.smoothScaleY_fp = interpolationStep(ctx.srcHeight, outHeight);
ctx.smoothNextOutY = 0;
ctx.smoothPrevY = -1;
ctx.error = false; ctx.error = false;
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale // MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale
@@ -512,7 +615,20 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
return false; return false;
} }
if (needsScaling) { if (smoothUpscale) {
// One contiguous allocation avoids three heap blocks while keeping smoothing line-buffered.
const size_t smoothRowsBytes = static_cast<size_t>(outWidth) * 3;
ctx.smoothRows = makeUniqueNoThrow<uint8_t[]>(smoothRowsBytes);
if (!ctx.smoothRows) {
LOG_ERR("JPG", "OOM: progressive smoothing buffers");
return false;
}
ctx.smoothPrevRow = ctx.smoothRows.get();
ctx.smoothCurrRow = ctx.smoothPrevRow + outWidth;
ctx.smoothOutRow = ctx.smoothCurrRow + outWidth;
LOG_DBG("JPG", "Progressive smoothing: %dx%d -> %dx%d, buffers=%u bytes", ctx.srcWidth, ctx.srcHeight, outWidth,
outHeight, static_cast<unsigned>(smoothRowsBytes));
} else if (needsScaling) {
ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth); ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth);
ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth); ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth);
if (!ctx.rowAccum || !ctx.rowCount) { if (!ctx.rowAccum || !ctx.rowCount) {
@@ -549,6 +665,10 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
rc = jpeg->decode(0, 0, 0); rc = jpeg->decode(0, 0, 0);
if (rc == 1 && ctx.smoothUpscale && !ctx.error) {
finishSmoothUpscale(&ctx);
}
if (rc != 1 || ctx.error) { if (rc != 1 || ctx.error) {
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
return false; return false;
+10 -5
View File
@@ -278,13 +278,18 @@ class XPathParagraphResolver final : public Print {
path.push_back({name, siblingIndex}); path.push_back({name, siblingIndex});
parentStates.emplace_back(); parentStates.emplace_back();
// Count both <p> and <li> as paragraph-like positions, matching how the section
// layout tracks them (xpathParagraphIndex and xpathListItemIndex). This ensures
// KOReader progress in list items maps to the correct XPath.
if (name == "p") { if (name == "p") {
paragraphCount++; paragraphCount++;
if (paragraphCount == targetParagraph) { } else if (name == "li") {
xpath = buildParagraphXPath(spineIndex, path, 0, 0); paragraphCount++;
stopped = true; }
XML_StopParser(parser, XML_FALSE); if (paragraphCount == targetParagraph) {
} xpath = buildParagraphXPath(spineIndex, path, 0, 0);
stopped = true;
XML_StopParser(parser, XML_FALSE);
} }
depth++; depth++;
+189 -51
View File
@@ -30,8 +30,7 @@ int parseIndex(const std::string& xpath, const char* prefix, bool last = false)
int parseCharOffset(const std::string& xpath) { int parseCharOffset(const std::string& xpath) {
const size_t textPos = xpath.rfind("text()"); const size_t textPos = xpath.rfind("text()");
if (textPos == std::string::npos) return 0; const size_t dotPos = (textPos != std::string::npos) ? xpath.find('.', textPos) : xpath.rfind('.');
const size_t dotPos = xpath.find('.', textPos);
if (dotPos == std::string::npos || dotPos + 1 >= xpath.size()) return 0; if (dotPos == std::string::npos || dotPos + 1 >= xpath.size()) return 0;
int val = 0; int val = 0;
for (size_t i = dotPos + 1; i < xpath.size(); i++) { for (size_t i = dotPos + 1; i < xpath.size(); i++) {
@@ -104,7 +103,13 @@ bool isChapterStartXPath(const std::string& xpath) {
if (dotPos == std::string::npos || dotPos <= bodyContentStart || dotPos + 1 >= xpath.size()) { if (dotPos == std::string::npos || dotPos <= bodyContentStart || dotPos + 1 >= xpath.size()) {
return false; return false;
} }
if (xpath.find('/', bodyContentStart) != std::string::npos) { size_t terminalEnd = dotPos;
static constexpr char kTextNode[] = "/text()";
const size_t textNodePos = xpath.rfind(kTextNode, dotPos);
if (textNodePos != std::string::npos && textNodePos >= bodyContentStart) {
terminalEnd = textNodePos;
}
if (xpath.find('/', bodyContentStart) < terminalEnd) {
return false; return false;
} }
@@ -122,7 +127,7 @@ struct XPathStep {
static constexpr int MAX_XPATH_DEPTH = 16; static constexpr int MAX_XPATH_DEPTH = 16;
// Parse the XPath segment between /body/DocFragment[N]/body/ and text()[N].offset // Parse the XPath segment between /body/DocFragment[N]/body/ and the terminal position
// into an ordered sequence of steps. Returns step count, 0 on failure. // into an ordered sequence of steps. Returns step count, 0 on failure.
// Example input: "/body/DocFragment[1]/body/div[1]/ul/li[4]/text()[1].51" // Example input: "/body/DocFragment[1]/body/div[1]/ul/li[4]/text()[1].51"
// Fills steps with: {div,1}, {ul,1}, {li,4} // Fills steps with: {div,1}, {ul,1}, {li,4}
@@ -136,13 +141,20 @@ int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH])
if (xpath.compare(afterBracket + 1, strlen(kBody), kBody) != 0) return 0; if (xpath.compare(afterBracket + 1, strlen(kBody), kBody) != 0) return 0;
size_t pos = afterBracket + 1 + strlen(kBody); size_t pos = afterBracket + 1 + strlen(kBody);
const size_t textPos = xpath.rfind("/text()"); size_t stepsEnd = xpath.rfind("/text()");
if (textPos == std::string::npos || textPos <= pos) return 0; if (stepsEnd == std::string::npos) {
stepsEnd = xpath.rfind('.');
if (stepsEnd == std::string::npos || stepsEnd <= pos || stepsEnd + 1 >= xpath.size()) return 0;
for (size_t i = stepsEnd + 1; i < xpath.size(); i++) {
if (xpath[i] < '0' || xpath[i] > '9') return 0;
}
}
if (stepsEnd <= pos) return 0;
int count = 0; int count = 0;
while (pos < textPos && count < MAX_XPATH_DEPTH) { while (pos < stepsEnd && count < MAX_XPATH_DEPTH) {
const size_t slash = xpath.find('/', pos); const size_t slash = xpath.find('/', pos);
const size_t segEnd = (slash < textPos) ? slash : textPos; const size_t segEnd = (slash < stepsEnd) ? slash : stepsEnd;
XPathStep& step = steps[count]; XPathStep& step = steps[count];
const size_t bracket = xpath.find('[', pos); const size_t bracket = xpath.find('[', pos);
@@ -166,7 +178,7 @@ int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH])
} }
count++; count++;
pos = (slash < textPos) ? slash + 1 : textPos; pos = (slash < stepsEnd) ? slash + 1 : stepsEnd;
} }
return count; return count;
} }
@@ -223,10 +235,148 @@ class ParagraphStreamer final : public Print {
char capturedAnchorId[MAX_ANCHOR_ID] = {}; char capturedAnchorId[MAX_ANCHOR_ID] = {};
int capturedAnchorIdLen = 0; int capturedAnchorIdLen = 0;
bool capturingAnchorTag = false; bool capturingAnchorTag = false;
enum IdScanState { ID_SCAN, ID_I, ID_D, ID_EQ, ID_IN_VALUE_D, ID_IN_VALUE_S } idState = ID_SCAN; enum AnchorAttrState {
ATTR_FIND_NAME,
ATTR_READ_NAME,
ATTR_AFTER_NAME,
ATTR_BEFORE_VALUE,
ATTR_CAPTURE_D,
ATTR_CAPTURE_S
} attrState = ATTR_FIND_NAME;
uint8_t attrNameLen = 0;
bool currentAttrIsId = false;
bool inAttrQuote = bool inAttrQuote =
false; // true while inside a quoted attribute value (prevents '/' from being treated as self-close) false; // true while inside a quoted attribute value (prevents '/' from being treated as self-close)
char attrQuoteChar = 0; char attrQuoteChar = 0;
uint8_t nonVisibleDepth = 0;
bool isNonVisibleTag() const {
return strcasecmp(tagName, "head") == 0 || strcasecmp(tagName, "style") == 0 ||
strcasecmp(tagName, "script") == 0 || strcasecmp(tagName, "title") == 0;
}
static bool isAttrWhitespace(uint8_t c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
static bool isAttrNameChar(uint8_t c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' ||
c == ':' || c == '.';
}
void resetAnchorAttrScan() {
attrState = ATTR_FIND_NAME;
attrNameLen = 0;
currentAttrIsId = false;
}
void finishCapturedAnchorId() {
capturedAnchorId[capturedAnchorIdLen] = '\0';
capturingAnchorTag = false;
resetAnchorAttrScan();
}
void beginAnchorIdScan() {
capturingAnchorTag = true;
resetAnchorAttrScan();
}
void endAnchorIdScan() {
if (capturingAnchorTag) {
capturedAnchorIdLen = 0;
}
capturingAnchorTag = false;
resetAnchorAttrScan();
}
void appendCapturedAnchorId(uint8_t c) {
if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID) {
capturedAnchorId[capturedAnchorIdLen++] = c;
}
}
void scanAnchorAttribute(uint8_t c) {
switch (attrState) {
case ATTR_FIND_NAME:
if (isAttrNameChar(c)) {
attrState = ATTR_READ_NAME;
attrNameLen = 1;
currentAttrIsId = c == 'i';
}
break;
case ATTR_READ_NAME:
if (isAttrNameChar(c)) {
if (attrNameLen == 1) {
currentAttrIsId = currentAttrIsId && c == 'd';
} else {
currentAttrIsId = false;
}
attrNameLen++;
} else {
currentAttrIsId = currentAttrIsId && attrNameLen == 2;
if (isAttrWhitespace(c)) {
attrState = ATTR_AFTER_NAME;
} else if (c == '=') {
attrState = ATTR_BEFORE_VALUE;
} else {
resetAnchorAttrScan();
}
}
break;
case ATTR_AFTER_NAME:
if (isAttrWhitespace(c)) {
break;
}
if (c == '=') {
attrState = ATTR_BEFORE_VALUE;
} else if (isAttrNameChar(c)) {
attrState = ATTR_READ_NAME;
attrNameLen = 1;
currentAttrIsId = c == 'i';
} else {
resetAnchorAttrScan();
}
break;
case ATTR_BEFORE_VALUE:
if (isAttrWhitespace(c)) {
break;
}
if (currentAttrIsId && c == '"') {
capturedAnchorIdLen = 0;
attrState = ATTR_CAPTURE_D;
} else if (currentAttrIsId && c == '\'') {
capturedAnchorIdLen = 0;
attrState = ATTR_CAPTURE_S;
} else if (c == '"') {
attrState = ATTR_CAPTURE_D;
} else if (c == '\'') {
attrState = ATTR_CAPTURE_S;
} else {
resetAnchorAttrScan();
}
break;
case ATTR_CAPTURE_D:
if (c == '"') {
if (currentAttrIsId) {
finishCapturedAnchorId();
} else {
resetAnchorAttrScan();
}
} else if (currentAttrIsId) {
appendCapturedAnchorId(c);
}
break;
case ATTR_CAPTURE_S:
if (c == '\'') {
if (currentAttrIsId) {
finishCapturedAnchorId();
} else {
resetAnchorAttrScan();
}
} else if (currentAttrIsId) {
appendCapturedAnchorId(c);
}
break;
}
}
void onVisibleCodepoint() { void onVisibleCodepoint() {
totalVisChars++; totalVisChars++;
@@ -286,15 +436,19 @@ class ParagraphStreamer final : public Print {
void onOpenTag() { void onOpenTag() {
htmlDepth++; htmlDepth++;
if (nonVisibleDepth > 0 || isNonVisibleTag()) {
nonVisibleDepth++;
return;
}
if (stepCount == 0) { if (stepCount == 0) {
if (strcasecmp(tagName, "p") == 0) onLegacyP(); if (strcasecmp(tagName, "p") == 0) onLegacyP();
return; return;
} }
// Capture <a id> inside the fully-matched element even after target char is found // Capture a child <a id> inside the fully-matched element even after target char is found.
if (revPFound && matchedDepth == stepCount && capturedAnchorIdLen == 0 && strcasecmp(tagName, "a") == 0) { if (revPFound && matchedDepth == stepCount && capturedAnchorIdLen == 0 && strcasecmp(tagName, "a") == 0) {
capturingAnchorTag = true; beginAnchorIdScan();
idState = ID_SCAN;
} }
if (revDone) return; if (revDone) return;
@@ -315,6 +469,7 @@ class ParagraphStreamer final : public Print {
stepEnteredAtDepth[matchedDepth] = htmlDepth; stepEnteredAtDepth[matchedDepth] = htmlDepth;
matchedDepth++; matchedDepth++;
if (matchedDepth == stepCount) { if (matchedDepth == stepCount) {
beginAnchorIdScan();
paragraphAtMatch = pCount; paragraphAtMatch = pCount;
liCountAtMatch = liCount; liCountAtMatch = liCount;
revPFound = true; revPFound = true;
@@ -332,6 +487,12 @@ class ParagraphStreamer final : public Print {
} }
void onCloseTag() { void onCloseTag() {
if (nonVisibleDepth > 0) {
nonVisibleDepth--;
if (htmlDepth > 0) htmlDepth--;
return;
}
// Legacy mode: each direct child element closing advances the text node index. // Legacy mode: each direct child element closing advances the text node index.
if (stepCount == 0 && revPFound && !revDone && paragraphHtmlDepth >= 0 && htmlDepth == paragraphHtmlDepth + 1) { if (stepCount == 0 && revPFound && !revDone && paragraphHtmlDepth >= 0 && htmlDepth == paragraphHtmlDepth + 1) {
currentTextNode++; currentTextNode++;
@@ -419,42 +580,12 @@ class ParagraphStreamer final : public Print {
attrQuoteChar = 0; attrQuoteChar = 0;
} }
if (capturingAnchorTag) { if (capturingAnchorTag) {
switch (idState) { scanAnchorAttribute(c);
case ID_SCAN:
idState = (c == 'i' || c == 'I') ? ID_I : ID_SCAN;
break;
case ID_I:
idState = (c == 'd' || c == 'D') ? ID_D : ID_SCAN;
break;
case ID_D:
idState = (c == '=') ? ID_EQ : ID_SCAN;
break;
case ID_EQ:
if (c == '"')
idState = ID_IN_VALUE_D;
else if (c == '\'')
idState = ID_IN_VALUE_S;
break;
case ID_IN_VALUE_D:
if (c == '"') {
capturedAnchorId[capturedAnchorIdLen] = '\0';
capturingAnchorTag = false;
} else if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID)
capturedAnchorId[capturedAnchorIdLen++] = c;
break;
case ID_IN_VALUE_S:
if (c == '\'') {
capturedAnchorId[capturedAnchorIdLen] = '\0';
capturingAnchorTag = false;
} else if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID)
capturedAnchorId[capturedAnchorIdLen++] = c;
break;
}
} }
// Only treat '/' as self-closing when outside a quoted attribute value. // Only treat '/' as self-closing when outside a quoted attribute value.
if (c == '/' && !inAttrQuote) { if (c == '/' && !inAttrQuote) {
endAnchorIdScan();
onCloseTag(); onCloseTag();
capturingAnchorTag = false;
} }
break; break;
} }
@@ -512,10 +643,13 @@ class ParagraphStreamer final : public Print {
tagNameLen = 0; tagNameLen = 0;
tagIsClose = false; tagIsClose = false;
capturingAnchorTag = false; capturingAnchorTag = false;
idState = ID_SCAN; resetAnchorAttrScan();
inAttrQuote = false; inAttrQuote = false;
attrQuoteChar = 0; attrQuoteChar = 0;
} else if (c == '>') { } else if (c == '>') {
if (tagState == TAG_ATTRS) {
endAnchorIdScan();
}
globalInTag = false; globalInTag = false;
inAttrQuote = false; inAttrQuote = false;
if (tagState == TAG_IN_NAME && tagNameLen > 0) { if (tagState == TAG_IN_NAME && tagNameLen > 0) {
@@ -529,6 +663,9 @@ class ParagraphStreamer final : public Print {
tagState = TAG_IDLE; tagState = TAG_IDLE;
} else if (globalInTag) { } else if (globalInTag) {
processByteInTag(c); processByteInTag(c);
} else if (nonVisibleDepth > 0) {
// Ignore head/style/script/title text. KOReader XPaths are body-relative, and CSS text
// should not contribute to intra-spine progress.
} else { } else {
if (c == '&') { if (c == '&') {
globalInEntity = true; globalInEntity = true;
@@ -572,12 +709,13 @@ SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub
float intra = float intra =
(pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f; (pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f;
result.percentage = epub->calculateProgress(pos.spineIndex, intra); result.percentage = epub->calculateProgress(pos.spineIndex, intra);
// Progress-based XPath correctly handles both <p> and <li> positions. if (pos.hasParagraphIndex && pos.paragraphIndex > 0) {
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
// Fall back to paragraph-index lookup when progress-based resolution fails.
if (result.xpath.empty() && pos.hasParagraphIndex && pos.paragraphIndex > 0) {
result.xpath = ChapterXPathResolver::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex); result.xpath = ChapterXPathResolver::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex);
} }
// Fall back to progress-based XPath, then synthetic progress mapping.
if (result.xpath.empty()) {
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
}
if (result.xpath.empty()) { if (result.xpath.empty()) {
result.xpath = generateXPath(epub, pos.spineIndex, intra); result.xpath = generateXPath(epub, pos.spineIndex, intra);
} }
@@ -762,4 +900,4 @@ std::string ProgressMapper::generateXPath(const std::shared_ptr<Epub>& epub, int
const int p = s.paragraphCount(); const int p = s.paragraphCount();
return (p > 0) ? base + "/p[" + std::to_string(p) + "]" : base; return (p > 0) ? base + "/p[" + std::to_string(p) + "]" : base;
} }
+5 -4
View File
@@ -10,7 +10,11 @@ OpdsParser::OpdsParser() {
if (!parser) { if (!parser) {
errorOccured = true; errorOccured = true;
LOG_DBG("OPDS", "Couldn't allocate memory for parser"); LOG_DBG("OPDS", "Couldn't allocate memory for parser");
return;
} }
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
} }
OpdsParser::~OpdsParser() { destroyXmlParser(parser); } OpdsParser::~OpdsParser() { destroyXmlParser(parser); }
@@ -20,10 +24,6 @@ size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
if (errorOccured) return length; if (errorOccured) return length;
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
const char* currentPos = reinterpret_cast<const char*>(xmlData); const char* currentPos = reinterpret_cast<const char*>(xmlData);
size_t remaining = length; size_t remaining = length;
constexpr size_t chunkSize = 1024; constexpr size_t chunkSize = 1024;
@@ -54,6 +54,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
} }
void OpdsParser::flush() { void OpdsParser::flush() {
if (errorOccured || !parser) return;
if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) { if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) {
errorOccured = true; errorOccured = true;
destroyXmlParser(parser); destroyXmlParser(parser);
+1 -1
View File
@@ -42,7 +42,7 @@ std::string Txt::getTitle() const {
// Remove .txt extension // Remove .txt extension
if (FsHelpers::hasTxtExtension(filename)) { if (FsHelpers::hasTxtExtension(filename)) {
filename = filename.substr(0, filename.length() - 4); filename.resize(filename.length() - 4);
} }
return filename; return filename;
+68
View File
@@ -1,5 +1,73 @@
#include "Utf8.h" #include "Utf8.h"
#include "Utf8ComposeTable.h"
namespace {
// Look up the canonical composition of (base + combining mark), or 0 if none.
uint32_t utf8ComposePair(const uint32_t base, const uint32_t mark) {
if (base > 0xFFFF || mark > 0xFFFF) return 0;
int lo = 0;
int hi = kUtf8ComposeTableSize - 1;
while (lo <= hi) {
const int mid = (lo + hi) / 2;
const Utf8ComposeEntry& e = kUtf8ComposeTable[mid];
if (e.base < base || (e.base == base && e.mark < mark)) {
lo = mid + 1;
} else if (e.base > base || (e.base == base && e.mark > mark)) {
hi = mid - 1;
} else {
return e.composed;
}
}
return 0;
}
} // namespace
std::string utf8ComposeNfc(const std::string& in) {
// Fast path: NFC composition can only change text that contains a combining
// diacritical mark U+0300-036F (UTF-8 lead byte 0xCC or 0xCD). Plain ASCII and
// already-precomposed (NFC) text -- the vast majority of words -- have none, so
// return them untouched without walking codepoints or allocating. A 0xCD that is
// actually a non-combining codepoint just falls through to the full pass below.
bool maybeHasMarks = false;
for (const unsigned char c : in) {
if (c == 0xCC || c == 0xCD) {
maybeHasMarks = true;
break;
}
}
if (!maybeHasMarks) return in;
std::string out;
out.reserve(in.size());
const unsigned char* p = reinterpret_cast<const unsigned char*>(in.c_str());
uint32_t base = 0;
bool haveBase = false;
while (*p) {
const uint32_t cp = utf8NextCodepoint(&p);
if (cp == 0) break;
if (utf8IsCombiningMark(cp)) {
const uint32_t composed = haveBase ? utf8ComposePair(base, cp) : 0;
if (composed) {
base = composed; // keep accumulating further marks onto the composed char
continue;
}
// No composition: flush the pending base, then emit the mark unchanged.
if (haveBase) {
utf8AppendCodepoint(base, out);
haveBase = false;
}
utf8AppendCodepoint(cp, out);
} else {
if (haveBase) utf8AppendCodepoint(base, out);
base = cp;
haveBase = true;
}
}
if (haveBase) utf8AppendCodepoint(base, out);
return out;
}
int utf8CodepointLen(const unsigned char c) { int utf8CodepointLen(const unsigned char c) {
if (c < 0x80) return 1; // 0xxxxxxx if (c < 0x80) return 1; // 0xxxxxxx
if ((c >> 5) == 0x6) return 2; // 110xxxxx if ((c >> 5) == 0x6) return 2; // 110xxxxx
+10 -1
View File
@@ -12,6 +12,12 @@ size_t utf8RemoveLastChar(std::string& str);
// Truncate string by removing N UTF-8 codepoints from the end. // Truncate string by removing N UTF-8 codepoints from the end.
void utf8TruncateChars(std::string& str, size_t numChars); void utf8TruncateChars(std::string& str, size_t numChars);
// Canonical composition (NFC) for the Latin / Vietnamese range: precomposes a
// base letter followed by combining diacritical mark(s) into a single codepoint.
// Needed because the device fonts have no combining-mark positioning, so text
// stored in NFD (e.g. some EPUB chapter titles) otherwise renders broken.
std::string utf8ComposeNfc(const std::string& in);
// Truncate a raw char buffer to the last complete UTF-8 codepoint boundary. // Truncate a raw char buffer to the last complete UTF-8 codepoint boundary.
// Returns the new length (<= len). If the buffer ends mid-sequence, the // Returns the new length (<= len). If the buffer ends mid-sequence, the
// incomplete trailing bytes are excluded. // incomplete trailing bytes are excluded.
@@ -21,12 +27,15 @@ int utf8SafeTruncateBuffer(const char* buf, int len);
// Covers CJK Unified Ideographs, Hiragana, Katakana, Hangul Syllables, CJK punctuation, // Covers CJK Unified Ideographs, Hiragana, Katakana, Hangul Syllables, CJK punctuation,
// and fullwidth forms — the ranges where word boundaries are implicit per character. // and fullwidth forms — the ranges where word boundaries are implicit per character.
inline bool utf8IsCjkBreakable(const uint32_t cp) { inline bool utf8IsCjkBreakable(const uint32_t cp) {
return (cp >= 0x3000 && cp <= 0x303F) // CJK Symbols and Punctuation return (cp >= 0x1100 && cp <= 0x11FF) // Hangul Jamo
|| (cp >= 0x3000 && cp <= 0x303F) // CJK Symbols and Punctuation
|| (cp >= 0x3040 && cp <= 0x309F) // Hiragana || (cp >= 0x3040 && cp <= 0x309F) // Hiragana
|| (cp >= 0x30A0 && cp <= 0x30FF) // Katakana || (cp >= 0x30A0 && cp <= 0x30FF) // Katakana
|| (cp >= 0x3130 && cp <= 0x318F) // Hangul Compatibility Jamo
|| (cp >= 0x3400 && cp <= 0x4DBF) // CJK Extension A || (cp >= 0x3400 && cp <= 0x4DBF) // CJK Extension A
|| (cp >= 0x4E00 && cp <= 0x9FFF) // CJK Unified Ideographs || (cp >= 0x4E00 && cp <= 0x9FFF) // CJK Unified Ideographs
|| (cp >= 0xAC00 && cp <= 0xD7AF) // Hangul Syllables || (cp >= 0xAC00 && cp <= 0xD7AF) // Hangul Syllables
|| (cp >= 0xD7B0 && cp <= 0xD7FF) // Hangul Jamo Extended-B
|| (cp >= 0xF900 && cp <= 0xFAFF) // CJK Compatibility Ideographs || (cp >= 0xF900 && cp <= 0xFAFF) // CJK Compatibility Ideographs
|| (cp >= 0xFE30 && cp <= 0xFE4F) // CJK Compatibility Forms || (cp >= 0xFE30 && cp <= 0xFE4F) // CJK Compatibility Forms
|| (cp >= 0xFF01 && cp <= 0xFF60) // Fullwidth Latin / Punctuation || (cp >= 0xFF01 && cp <= 0xFF60) // Fullwidth Latin / Punctuation
+229
View File
@@ -0,0 +1,229 @@
// Auto-generated canonical composition (NFC) table for the Latin / Vietnamese
// range (combining marks U+0300-U+036F). Generated from Python unicodedata.
// Used by utf8ComposeNfc() to precompose decomposed (NFD) text so the device
// fonts (which lack combining-mark positioning) render it correctly.
#pragma once
#include <cstdint>
struct Utf8ComposeEntry {
uint16_t base;
uint16_t mark;
uint16_t composed;
};
// Sorted by (base, mark) for binary search.
static constexpr Utf8ComposeEntry kUtf8ComposeTable[] = {
{0x0041, 0x0300, 0x00C0}, {0x0041, 0x0301, 0x00C1}, {0x0041, 0x0302, 0x00C2}, {0x0041, 0x0303, 0x00C3},
{0x0041, 0x0304, 0x0100}, {0x0041, 0x0306, 0x0102}, {0x0041, 0x0307, 0x0226}, {0x0041, 0x0308, 0x00C4},
{0x0041, 0x0309, 0x1EA2}, {0x0041, 0x030A, 0x00C5}, {0x0041, 0x030C, 0x01CD}, {0x0041, 0x030F, 0x0200},
{0x0041, 0x0311, 0x0202}, {0x0041, 0x0323, 0x1EA0}, {0x0041, 0x0325, 0x1E00}, {0x0041, 0x0328, 0x0104},
{0x0041, 0x0340, 0x00C0}, {0x0041, 0x0341, 0x00C1}, {0x0042, 0x0307, 0x1E02}, {0x0042, 0x0323, 0x1E04},
{0x0042, 0x0331, 0x1E06}, {0x0043, 0x0301, 0x0106}, {0x0043, 0x0302, 0x0108}, {0x0043, 0x0307, 0x010A},
{0x0043, 0x030C, 0x010C}, {0x0043, 0x0327, 0x00C7}, {0x0043, 0x0341, 0x0106}, {0x0044, 0x0307, 0x1E0A},
{0x0044, 0x030C, 0x010E}, {0x0044, 0x0323, 0x1E0C}, {0x0044, 0x0327, 0x1E10}, {0x0044, 0x032D, 0x1E12},
{0x0044, 0x0331, 0x1E0E}, {0x0045, 0x0300, 0x00C8}, {0x0045, 0x0301, 0x00C9}, {0x0045, 0x0302, 0x00CA},
{0x0045, 0x0303, 0x1EBC}, {0x0045, 0x0304, 0x0112}, {0x0045, 0x0306, 0x0114}, {0x0045, 0x0307, 0x0116},
{0x0045, 0x0308, 0x00CB}, {0x0045, 0x0309, 0x1EBA}, {0x0045, 0x030C, 0x011A}, {0x0045, 0x030F, 0x0204},
{0x0045, 0x0311, 0x0206}, {0x0045, 0x0323, 0x1EB8}, {0x0045, 0x0327, 0x0228}, {0x0045, 0x0328, 0x0118},
{0x0045, 0x032D, 0x1E18}, {0x0045, 0x0330, 0x1E1A}, {0x0045, 0x0340, 0x00C8}, {0x0045, 0x0341, 0x00C9},
{0x0046, 0x0307, 0x1E1E}, {0x0047, 0x0301, 0x01F4}, {0x0047, 0x0302, 0x011C}, {0x0047, 0x0304, 0x1E20},
{0x0047, 0x0306, 0x011E}, {0x0047, 0x0307, 0x0120}, {0x0047, 0x030C, 0x01E6}, {0x0047, 0x0327, 0x0122},
{0x0047, 0x0341, 0x01F4}, {0x0048, 0x0302, 0x0124}, {0x0048, 0x0307, 0x1E22}, {0x0048, 0x0308, 0x1E26},
{0x0048, 0x030C, 0x021E}, {0x0048, 0x0323, 0x1E24}, {0x0048, 0x0327, 0x1E28}, {0x0048, 0x032E, 0x1E2A},
{0x0049, 0x0300, 0x00CC}, {0x0049, 0x0301, 0x00CD}, {0x0049, 0x0302, 0x00CE}, {0x0049, 0x0303, 0x0128},
{0x0049, 0x0304, 0x012A}, {0x0049, 0x0306, 0x012C}, {0x0049, 0x0307, 0x0130}, {0x0049, 0x0308, 0x00CF},
{0x0049, 0x0309, 0x1EC8}, {0x0049, 0x030C, 0x01CF}, {0x0049, 0x030F, 0x0208}, {0x0049, 0x0311, 0x020A},
{0x0049, 0x0323, 0x1ECA}, {0x0049, 0x0328, 0x012E}, {0x0049, 0x0330, 0x1E2C}, {0x0049, 0x0340, 0x00CC},
{0x0049, 0x0341, 0x00CD}, {0x0049, 0x0344, 0x1E2E}, {0x004A, 0x0302, 0x0134}, {0x004B, 0x0301, 0x1E30},
{0x004B, 0x030C, 0x01E8}, {0x004B, 0x0323, 0x1E32}, {0x004B, 0x0327, 0x0136}, {0x004B, 0x0331, 0x1E34},
{0x004B, 0x0341, 0x1E30}, {0x004C, 0x0301, 0x0139}, {0x004C, 0x030C, 0x013D}, {0x004C, 0x0323, 0x1E36},
{0x004C, 0x0327, 0x013B}, {0x004C, 0x032D, 0x1E3C}, {0x004C, 0x0331, 0x1E3A}, {0x004C, 0x0341, 0x0139},
{0x004D, 0x0301, 0x1E3E}, {0x004D, 0x0307, 0x1E40}, {0x004D, 0x0323, 0x1E42}, {0x004D, 0x0341, 0x1E3E},
{0x004E, 0x0300, 0x01F8}, {0x004E, 0x0301, 0x0143}, {0x004E, 0x0303, 0x00D1}, {0x004E, 0x0307, 0x1E44},
{0x004E, 0x030C, 0x0147}, {0x004E, 0x0323, 0x1E46}, {0x004E, 0x0327, 0x0145}, {0x004E, 0x032D, 0x1E4A},
{0x004E, 0x0331, 0x1E48}, {0x004E, 0x0340, 0x01F8}, {0x004E, 0x0341, 0x0143}, {0x004F, 0x0300, 0x00D2},
{0x004F, 0x0301, 0x00D3}, {0x004F, 0x0302, 0x00D4}, {0x004F, 0x0303, 0x00D5}, {0x004F, 0x0304, 0x014C},
{0x004F, 0x0306, 0x014E}, {0x004F, 0x0307, 0x022E}, {0x004F, 0x0308, 0x00D6}, {0x004F, 0x0309, 0x1ECE},
{0x004F, 0x030B, 0x0150}, {0x004F, 0x030C, 0x01D1}, {0x004F, 0x030F, 0x020C}, {0x004F, 0x0311, 0x020E},
{0x004F, 0x031B, 0x01A0}, {0x004F, 0x0323, 0x1ECC}, {0x004F, 0x0328, 0x01EA}, {0x004F, 0x0340, 0x00D2},
{0x004F, 0x0341, 0x00D3}, {0x0050, 0x0301, 0x1E54}, {0x0050, 0x0307, 0x1E56}, {0x0050, 0x0341, 0x1E54},
{0x0052, 0x0301, 0x0154}, {0x0052, 0x0307, 0x1E58}, {0x0052, 0x030C, 0x0158}, {0x0052, 0x030F, 0x0210},
{0x0052, 0x0311, 0x0212}, {0x0052, 0x0323, 0x1E5A}, {0x0052, 0x0327, 0x0156}, {0x0052, 0x0331, 0x1E5E},
{0x0052, 0x0341, 0x0154}, {0x0053, 0x0301, 0x015A}, {0x0053, 0x0302, 0x015C}, {0x0053, 0x0307, 0x1E60},
{0x0053, 0x030C, 0x0160}, {0x0053, 0x0323, 0x1E62}, {0x0053, 0x0326, 0x0218}, {0x0053, 0x0327, 0x015E},
{0x0053, 0x0341, 0x015A}, {0x0054, 0x0307, 0x1E6A}, {0x0054, 0x030C, 0x0164}, {0x0054, 0x0323, 0x1E6C},
{0x0054, 0x0326, 0x021A}, {0x0054, 0x0327, 0x0162}, {0x0054, 0x032D, 0x1E70}, {0x0054, 0x0331, 0x1E6E},
{0x0055, 0x0300, 0x00D9}, {0x0055, 0x0301, 0x00DA}, {0x0055, 0x0302, 0x00DB}, {0x0055, 0x0303, 0x0168},
{0x0055, 0x0304, 0x016A}, {0x0055, 0x0306, 0x016C}, {0x0055, 0x0308, 0x00DC}, {0x0055, 0x0309, 0x1EE6},
{0x0055, 0x030A, 0x016E}, {0x0055, 0x030B, 0x0170}, {0x0055, 0x030C, 0x01D3}, {0x0055, 0x030F, 0x0214},
{0x0055, 0x0311, 0x0216}, {0x0055, 0x031B, 0x01AF}, {0x0055, 0x0323, 0x1EE4}, {0x0055, 0x0324, 0x1E72},
{0x0055, 0x0328, 0x0172}, {0x0055, 0x032D, 0x1E76}, {0x0055, 0x0330, 0x1E74}, {0x0055, 0x0340, 0x00D9},
{0x0055, 0x0341, 0x00DA}, {0x0055, 0x0344, 0x01D7}, {0x0056, 0x0303, 0x1E7C}, {0x0056, 0x0323, 0x1E7E},
{0x0057, 0x0300, 0x1E80}, {0x0057, 0x0301, 0x1E82}, {0x0057, 0x0302, 0x0174}, {0x0057, 0x0307, 0x1E86},
{0x0057, 0x0308, 0x1E84}, {0x0057, 0x0323, 0x1E88}, {0x0057, 0x0340, 0x1E80}, {0x0057, 0x0341, 0x1E82},
{0x0058, 0x0307, 0x1E8A}, {0x0058, 0x0308, 0x1E8C}, {0x0059, 0x0300, 0x1EF2}, {0x0059, 0x0301, 0x00DD},
{0x0059, 0x0302, 0x0176}, {0x0059, 0x0303, 0x1EF8}, {0x0059, 0x0304, 0x0232}, {0x0059, 0x0307, 0x1E8E},
{0x0059, 0x0308, 0x0178}, {0x0059, 0x0309, 0x1EF6}, {0x0059, 0x0323, 0x1EF4}, {0x0059, 0x0340, 0x1EF2},
{0x0059, 0x0341, 0x00DD}, {0x005A, 0x0301, 0x0179}, {0x005A, 0x0302, 0x1E90}, {0x005A, 0x0307, 0x017B},
{0x005A, 0x030C, 0x017D}, {0x005A, 0x0323, 0x1E92}, {0x005A, 0x0331, 0x1E94}, {0x005A, 0x0341, 0x0179},
{0x0061, 0x0300, 0x00E0}, {0x0061, 0x0301, 0x00E1}, {0x0061, 0x0302, 0x00E2}, {0x0061, 0x0303, 0x00E3},
{0x0061, 0x0304, 0x0101}, {0x0061, 0x0306, 0x0103}, {0x0061, 0x0307, 0x0227}, {0x0061, 0x0308, 0x00E4},
{0x0061, 0x0309, 0x1EA3}, {0x0061, 0x030A, 0x00E5}, {0x0061, 0x030C, 0x01CE}, {0x0061, 0x030F, 0x0201},
{0x0061, 0x0311, 0x0203}, {0x0061, 0x0323, 0x1EA1}, {0x0061, 0x0325, 0x1E01}, {0x0061, 0x0328, 0x0105},
{0x0061, 0x0340, 0x00E0}, {0x0061, 0x0341, 0x00E1}, {0x0062, 0x0307, 0x1E03}, {0x0062, 0x0323, 0x1E05},
{0x0062, 0x0331, 0x1E07}, {0x0063, 0x0301, 0x0107}, {0x0063, 0x0302, 0x0109}, {0x0063, 0x0307, 0x010B},
{0x0063, 0x030C, 0x010D}, {0x0063, 0x0327, 0x00E7}, {0x0063, 0x0341, 0x0107}, {0x0064, 0x0307, 0x1E0B},
{0x0064, 0x030C, 0x010F}, {0x0064, 0x0323, 0x1E0D}, {0x0064, 0x0327, 0x1E11}, {0x0064, 0x032D, 0x1E13},
{0x0064, 0x0331, 0x1E0F}, {0x0065, 0x0300, 0x00E8}, {0x0065, 0x0301, 0x00E9}, {0x0065, 0x0302, 0x00EA},
{0x0065, 0x0303, 0x1EBD}, {0x0065, 0x0304, 0x0113}, {0x0065, 0x0306, 0x0115}, {0x0065, 0x0307, 0x0117},
{0x0065, 0x0308, 0x00EB}, {0x0065, 0x0309, 0x1EBB}, {0x0065, 0x030C, 0x011B}, {0x0065, 0x030F, 0x0205},
{0x0065, 0x0311, 0x0207}, {0x0065, 0x0323, 0x1EB9}, {0x0065, 0x0327, 0x0229}, {0x0065, 0x0328, 0x0119},
{0x0065, 0x032D, 0x1E19}, {0x0065, 0x0330, 0x1E1B}, {0x0065, 0x0340, 0x00E8}, {0x0065, 0x0341, 0x00E9},
{0x0066, 0x0307, 0x1E1F}, {0x0067, 0x0301, 0x01F5}, {0x0067, 0x0302, 0x011D}, {0x0067, 0x0304, 0x1E21},
{0x0067, 0x0306, 0x011F}, {0x0067, 0x0307, 0x0121}, {0x0067, 0x030C, 0x01E7}, {0x0067, 0x0327, 0x0123},
{0x0067, 0x0341, 0x01F5}, {0x0068, 0x0302, 0x0125}, {0x0068, 0x0307, 0x1E23}, {0x0068, 0x0308, 0x1E27},
{0x0068, 0x030C, 0x021F}, {0x0068, 0x0323, 0x1E25}, {0x0068, 0x0327, 0x1E29}, {0x0068, 0x032E, 0x1E2B},
{0x0068, 0x0331, 0x1E96}, {0x0069, 0x0300, 0x00EC}, {0x0069, 0x0301, 0x00ED}, {0x0069, 0x0302, 0x00EE},
{0x0069, 0x0303, 0x0129}, {0x0069, 0x0304, 0x012B}, {0x0069, 0x0306, 0x012D}, {0x0069, 0x0308, 0x00EF},
{0x0069, 0x0309, 0x1EC9}, {0x0069, 0x030C, 0x01D0}, {0x0069, 0x030F, 0x0209}, {0x0069, 0x0311, 0x020B},
{0x0069, 0x0323, 0x1ECB}, {0x0069, 0x0328, 0x012F}, {0x0069, 0x0330, 0x1E2D}, {0x0069, 0x0340, 0x00EC},
{0x0069, 0x0341, 0x00ED}, {0x0069, 0x0344, 0x1E2F}, {0x006A, 0x0302, 0x0135}, {0x006A, 0x030C, 0x01F0},
{0x006B, 0x0301, 0x1E31}, {0x006B, 0x030C, 0x01E9}, {0x006B, 0x0323, 0x1E33}, {0x006B, 0x0327, 0x0137},
{0x006B, 0x0331, 0x1E35}, {0x006B, 0x0341, 0x1E31}, {0x006C, 0x0301, 0x013A}, {0x006C, 0x030C, 0x013E},
{0x006C, 0x0323, 0x1E37}, {0x006C, 0x0327, 0x013C}, {0x006C, 0x032D, 0x1E3D}, {0x006C, 0x0331, 0x1E3B},
{0x006C, 0x0341, 0x013A}, {0x006D, 0x0301, 0x1E3F}, {0x006D, 0x0307, 0x1E41}, {0x006D, 0x0323, 0x1E43},
{0x006D, 0x0341, 0x1E3F}, {0x006E, 0x0300, 0x01F9}, {0x006E, 0x0301, 0x0144}, {0x006E, 0x0303, 0x00F1},
{0x006E, 0x0307, 0x1E45}, {0x006E, 0x030C, 0x0148}, {0x006E, 0x0323, 0x1E47}, {0x006E, 0x0327, 0x0146},
{0x006E, 0x032D, 0x1E4B}, {0x006E, 0x0331, 0x1E49}, {0x006E, 0x0340, 0x01F9}, {0x006E, 0x0341, 0x0144},
{0x006F, 0x0300, 0x00F2}, {0x006F, 0x0301, 0x00F3}, {0x006F, 0x0302, 0x00F4}, {0x006F, 0x0303, 0x00F5},
{0x006F, 0x0304, 0x014D}, {0x006F, 0x0306, 0x014F}, {0x006F, 0x0307, 0x022F}, {0x006F, 0x0308, 0x00F6},
{0x006F, 0x0309, 0x1ECF}, {0x006F, 0x030B, 0x0151}, {0x006F, 0x030C, 0x01D2}, {0x006F, 0x030F, 0x020D},
{0x006F, 0x0311, 0x020F}, {0x006F, 0x031B, 0x01A1}, {0x006F, 0x0323, 0x1ECD}, {0x006F, 0x0328, 0x01EB},
{0x006F, 0x0340, 0x00F2}, {0x006F, 0x0341, 0x00F3}, {0x0070, 0x0301, 0x1E55}, {0x0070, 0x0307, 0x1E57},
{0x0070, 0x0341, 0x1E55}, {0x0072, 0x0301, 0x0155}, {0x0072, 0x0307, 0x1E59}, {0x0072, 0x030C, 0x0159},
{0x0072, 0x030F, 0x0211}, {0x0072, 0x0311, 0x0213}, {0x0072, 0x0323, 0x1E5B}, {0x0072, 0x0327, 0x0157},
{0x0072, 0x0331, 0x1E5F}, {0x0072, 0x0341, 0x0155}, {0x0073, 0x0301, 0x015B}, {0x0073, 0x0302, 0x015D},
{0x0073, 0x0307, 0x1E61}, {0x0073, 0x030C, 0x0161}, {0x0073, 0x0323, 0x1E63}, {0x0073, 0x0326, 0x0219},
{0x0073, 0x0327, 0x015F}, {0x0073, 0x0341, 0x015B}, {0x0074, 0x0307, 0x1E6B}, {0x0074, 0x0308, 0x1E97},
{0x0074, 0x030C, 0x0165}, {0x0074, 0x0323, 0x1E6D}, {0x0074, 0x0326, 0x021B}, {0x0074, 0x0327, 0x0163},
{0x0074, 0x032D, 0x1E71}, {0x0074, 0x0331, 0x1E6F}, {0x0075, 0x0300, 0x00F9}, {0x0075, 0x0301, 0x00FA},
{0x0075, 0x0302, 0x00FB}, {0x0075, 0x0303, 0x0169}, {0x0075, 0x0304, 0x016B}, {0x0075, 0x0306, 0x016D},
{0x0075, 0x0308, 0x00FC}, {0x0075, 0x0309, 0x1EE7}, {0x0075, 0x030A, 0x016F}, {0x0075, 0x030B, 0x0171},
{0x0075, 0x030C, 0x01D4}, {0x0075, 0x030F, 0x0215}, {0x0075, 0x0311, 0x0217}, {0x0075, 0x031B, 0x01B0},
{0x0075, 0x0323, 0x1EE5}, {0x0075, 0x0324, 0x1E73}, {0x0075, 0x0328, 0x0173}, {0x0075, 0x032D, 0x1E77},
{0x0075, 0x0330, 0x1E75}, {0x0075, 0x0340, 0x00F9}, {0x0075, 0x0341, 0x00FA}, {0x0075, 0x0344, 0x01D8},
{0x0076, 0x0303, 0x1E7D}, {0x0076, 0x0323, 0x1E7F}, {0x0077, 0x0300, 0x1E81}, {0x0077, 0x0301, 0x1E83},
{0x0077, 0x0302, 0x0175}, {0x0077, 0x0307, 0x1E87}, {0x0077, 0x0308, 0x1E85}, {0x0077, 0x030A, 0x1E98},
{0x0077, 0x0323, 0x1E89}, {0x0077, 0x0340, 0x1E81}, {0x0077, 0x0341, 0x1E83}, {0x0078, 0x0307, 0x1E8B},
{0x0078, 0x0308, 0x1E8D}, {0x0079, 0x0300, 0x1EF3}, {0x0079, 0x0301, 0x00FD}, {0x0079, 0x0302, 0x0177},
{0x0079, 0x0303, 0x1EF9}, {0x0079, 0x0304, 0x0233}, {0x0079, 0x0307, 0x1E8F}, {0x0079, 0x0308, 0x00FF},
{0x0079, 0x0309, 0x1EF7}, {0x0079, 0x030A, 0x1E99}, {0x0079, 0x0323, 0x1EF5}, {0x0079, 0x0340, 0x1EF3},
{0x0079, 0x0341, 0x00FD}, {0x007A, 0x0301, 0x017A}, {0x007A, 0x0302, 0x1E91}, {0x007A, 0x0307, 0x017C},
{0x007A, 0x030C, 0x017E}, {0x007A, 0x0323, 0x1E93}, {0x007A, 0x0331, 0x1E95}, {0x007A, 0x0341, 0x017A},
{0x00A8, 0x0300, 0x1FED}, {0x00A8, 0x0301, 0x0385}, {0x00A8, 0x0340, 0x1FED}, {0x00A8, 0x0341, 0x0385},
{0x00A8, 0x0342, 0x1FC1}, {0x00C2, 0x0300, 0x1EA6}, {0x00C2, 0x0301, 0x1EA4}, {0x00C2, 0x0303, 0x1EAA},
{0x00C2, 0x0309, 0x1EA8}, {0x00C2, 0x0323, 0x1EAC}, {0x00C2, 0x0340, 0x1EA6}, {0x00C2, 0x0341, 0x1EA4},
{0x00C4, 0x0304, 0x01DE}, {0x00C5, 0x0301, 0x01FA}, {0x00C5, 0x0341, 0x01FA}, {0x00C6, 0x0301, 0x01FC},
{0x00C6, 0x0304, 0x01E2}, {0x00C6, 0x0341, 0x01FC}, {0x00C7, 0x0301, 0x1E08}, {0x00C7, 0x0341, 0x1E08},
{0x00CA, 0x0300, 0x1EC0}, {0x00CA, 0x0301, 0x1EBE}, {0x00CA, 0x0303, 0x1EC4}, {0x00CA, 0x0309, 0x1EC2},
{0x00CA, 0x0323, 0x1EC6}, {0x00CA, 0x0340, 0x1EC0}, {0x00CA, 0x0341, 0x1EBE}, {0x00CF, 0x0301, 0x1E2E},
{0x00CF, 0x0341, 0x1E2E}, {0x00D2, 0x031B, 0x1EDC}, {0x00D3, 0x031B, 0x1EDA}, {0x00D4, 0x0300, 0x1ED2},
{0x00D4, 0x0301, 0x1ED0}, {0x00D4, 0x0303, 0x1ED6}, {0x00D4, 0x0309, 0x1ED4}, {0x00D4, 0x0323, 0x1ED8},
{0x00D4, 0x0340, 0x1ED2}, {0x00D4, 0x0341, 0x1ED0}, {0x00D5, 0x0301, 0x1E4C}, {0x00D5, 0x0304, 0x022C},
{0x00D5, 0x0308, 0x1E4E}, {0x00D5, 0x031B, 0x1EE0}, {0x00D5, 0x0341, 0x1E4C}, {0x00D6, 0x0304, 0x022A},
{0x00D8, 0x0301, 0x01FE}, {0x00D8, 0x0341, 0x01FE}, {0x00D9, 0x031B, 0x1EEA}, {0x00DA, 0x031B, 0x1EE8},
{0x00DC, 0x0300, 0x01DB}, {0x00DC, 0x0301, 0x01D7}, {0x00DC, 0x0304, 0x01D5}, {0x00DC, 0x030C, 0x01D9},
{0x00DC, 0x0340, 0x01DB}, {0x00DC, 0x0341, 0x01D7}, {0x00E2, 0x0300, 0x1EA7}, {0x00E2, 0x0301, 0x1EA5},
{0x00E2, 0x0303, 0x1EAB}, {0x00E2, 0x0309, 0x1EA9}, {0x00E2, 0x0323, 0x1EAD}, {0x00E2, 0x0340, 0x1EA7},
{0x00E2, 0x0341, 0x1EA5}, {0x00E4, 0x0304, 0x01DF}, {0x00E5, 0x0301, 0x01FB}, {0x00E5, 0x0341, 0x01FB},
{0x00E6, 0x0301, 0x01FD}, {0x00E6, 0x0304, 0x01E3}, {0x00E6, 0x0341, 0x01FD}, {0x00E7, 0x0301, 0x1E09},
{0x00E7, 0x0341, 0x1E09}, {0x00EA, 0x0300, 0x1EC1}, {0x00EA, 0x0301, 0x1EBF}, {0x00EA, 0x0303, 0x1EC5},
{0x00EA, 0x0309, 0x1EC3}, {0x00EA, 0x0323, 0x1EC7}, {0x00EA, 0x0340, 0x1EC1}, {0x00EA, 0x0341, 0x1EBF},
{0x00EF, 0x0301, 0x1E2F}, {0x00EF, 0x0341, 0x1E2F}, {0x00F2, 0x031B, 0x1EDD}, {0x00F3, 0x031B, 0x1EDB},
{0x00F4, 0x0300, 0x1ED3}, {0x00F4, 0x0301, 0x1ED1}, {0x00F4, 0x0303, 0x1ED7}, {0x00F4, 0x0309, 0x1ED5},
{0x00F4, 0x0323, 0x1ED9}, {0x00F4, 0x0340, 0x1ED3}, {0x00F4, 0x0341, 0x1ED1}, {0x00F5, 0x0301, 0x1E4D},
{0x00F5, 0x0304, 0x022D}, {0x00F5, 0x0308, 0x1E4F}, {0x00F5, 0x031B, 0x1EE1}, {0x00F5, 0x0341, 0x1E4D},
{0x00F6, 0x0304, 0x022B}, {0x00F8, 0x0301, 0x01FF}, {0x00F8, 0x0341, 0x01FF}, {0x00F9, 0x031B, 0x1EEB},
{0x00FA, 0x031B, 0x1EE9}, {0x00FC, 0x0300, 0x01DC}, {0x00FC, 0x0301, 0x01D8}, {0x00FC, 0x0304, 0x01D6},
{0x00FC, 0x030C, 0x01DA}, {0x00FC, 0x0340, 0x01DC}, {0x00FC, 0x0341, 0x01D8}, {0x0102, 0x0300, 0x1EB0},
{0x0102, 0x0301, 0x1EAE}, {0x0102, 0x0303, 0x1EB4}, {0x0102, 0x0309, 0x1EB2}, {0x0102, 0x0323, 0x1EB6},
{0x0102, 0x0340, 0x1EB0}, {0x0102, 0x0341, 0x1EAE}, {0x0103, 0x0300, 0x1EB1}, {0x0103, 0x0301, 0x1EAF},
{0x0103, 0x0303, 0x1EB5}, {0x0103, 0x0309, 0x1EB3}, {0x0103, 0x0323, 0x1EB7}, {0x0103, 0x0340, 0x1EB1},
{0x0103, 0x0341, 0x1EAF}, {0x0106, 0x0327, 0x1E08}, {0x0107, 0x0327, 0x1E09}, {0x0112, 0x0300, 0x1E14},
{0x0112, 0x0301, 0x1E16}, {0x0112, 0x0340, 0x1E14}, {0x0112, 0x0341, 0x1E16}, {0x0113, 0x0300, 0x1E15},
{0x0113, 0x0301, 0x1E17}, {0x0113, 0x0340, 0x1E15}, {0x0113, 0x0341, 0x1E17}, {0x0114, 0x0327, 0x1E1C},
{0x0115, 0x0327, 0x1E1D}, {0x014C, 0x0300, 0x1E50}, {0x014C, 0x0301, 0x1E52}, {0x014C, 0x0328, 0x01EC},
{0x014C, 0x0340, 0x1E50}, {0x014C, 0x0341, 0x1E52}, {0x014D, 0x0300, 0x1E51}, {0x014D, 0x0301, 0x1E53},
{0x014D, 0x0328, 0x01ED}, {0x014D, 0x0340, 0x1E51}, {0x014D, 0x0341, 0x1E53}, {0x015A, 0x0307, 0x1E64},
{0x015B, 0x0307, 0x1E65}, {0x0160, 0x0307, 0x1E66}, {0x0161, 0x0307, 0x1E67}, {0x0168, 0x0301, 0x1E78},
{0x0168, 0x031B, 0x1EEE}, {0x0168, 0x0341, 0x1E78}, {0x0169, 0x0301, 0x1E79}, {0x0169, 0x031B, 0x1EEF},
{0x0169, 0x0341, 0x1E79}, {0x016A, 0x0308, 0x1E7A}, {0x016B, 0x0308, 0x1E7B}, {0x017F, 0x0307, 0x1E9B},
{0x01A0, 0x0300, 0x1EDC}, {0x01A0, 0x0301, 0x1EDA}, {0x01A0, 0x0303, 0x1EE0}, {0x01A0, 0x0309, 0x1EDE},
{0x01A0, 0x0323, 0x1EE2}, {0x01A0, 0x0340, 0x1EDC}, {0x01A0, 0x0341, 0x1EDA}, {0x01A1, 0x0300, 0x1EDD},
{0x01A1, 0x0301, 0x1EDB}, {0x01A1, 0x0303, 0x1EE1}, {0x01A1, 0x0309, 0x1EDF}, {0x01A1, 0x0323, 0x1EE3},
{0x01A1, 0x0340, 0x1EDD}, {0x01A1, 0x0341, 0x1EDB}, {0x01AF, 0x0300, 0x1EEA}, {0x01AF, 0x0301, 0x1EE8},
{0x01AF, 0x0303, 0x1EEE}, {0x01AF, 0x0309, 0x1EEC}, {0x01AF, 0x0323, 0x1EF0}, {0x01AF, 0x0340, 0x1EEA},
{0x01AF, 0x0341, 0x1EE8}, {0x01B0, 0x0300, 0x1EEB}, {0x01B0, 0x0301, 0x1EE9}, {0x01B0, 0x0303, 0x1EEF},
{0x01B0, 0x0309, 0x1EED}, {0x01B0, 0x0323, 0x1EF1}, {0x01B0, 0x0340, 0x1EEB}, {0x01B0, 0x0341, 0x1EE9},
{0x01B7, 0x030C, 0x01EE}, {0x01EA, 0x0304, 0x01EC}, {0x01EB, 0x0304, 0x01ED}, {0x0226, 0x0304, 0x01E0},
{0x0227, 0x0304, 0x01E1}, {0x0228, 0x0306, 0x1E1C}, {0x0229, 0x0306, 0x1E1D}, {0x022E, 0x0304, 0x0230},
{0x022F, 0x0304, 0x0231}, {0x0292, 0x030C, 0x01EF}, {0x0391, 0x0300, 0x1FBA}, {0x0391, 0x0301, 0x0386},
{0x0391, 0x0304, 0x1FB9}, {0x0391, 0x0306, 0x1FB8}, {0x0391, 0x0313, 0x1F08}, {0x0391, 0x0314, 0x1F09},
{0x0391, 0x0340, 0x1FBA}, {0x0391, 0x0341, 0x0386}, {0x0391, 0x0343, 0x1F08}, {0x0391, 0x0345, 0x1FBC},
{0x0395, 0x0300, 0x1FC8}, {0x0395, 0x0301, 0x0388}, {0x0395, 0x0313, 0x1F18}, {0x0395, 0x0314, 0x1F19},
{0x0395, 0x0340, 0x1FC8}, {0x0395, 0x0341, 0x0388}, {0x0395, 0x0343, 0x1F18}, {0x0397, 0x0300, 0x1FCA},
{0x0397, 0x0301, 0x0389}, {0x0397, 0x0313, 0x1F28}, {0x0397, 0x0314, 0x1F29}, {0x0397, 0x0340, 0x1FCA},
{0x0397, 0x0341, 0x0389}, {0x0397, 0x0343, 0x1F28}, {0x0397, 0x0345, 0x1FCC}, {0x0399, 0x0300, 0x1FDA},
{0x0399, 0x0301, 0x038A}, {0x0399, 0x0304, 0x1FD9}, {0x0399, 0x0306, 0x1FD8}, {0x0399, 0x0308, 0x03AA},
{0x0399, 0x0313, 0x1F38}, {0x0399, 0x0314, 0x1F39}, {0x0399, 0x0340, 0x1FDA}, {0x0399, 0x0341, 0x038A},
{0x0399, 0x0343, 0x1F38}, {0x039F, 0x0300, 0x1FF8}, {0x039F, 0x0301, 0x038C}, {0x039F, 0x0313, 0x1F48},
{0x039F, 0x0314, 0x1F49}, {0x039F, 0x0340, 0x1FF8}, {0x039F, 0x0341, 0x038C}, {0x039F, 0x0343, 0x1F48},
{0x03A1, 0x0314, 0x1FEC}, {0x03A5, 0x0300, 0x1FEA}, {0x03A5, 0x0301, 0x038E}, {0x03A5, 0x0304, 0x1FE9},
{0x03A5, 0x0306, 0x1FE8}, {0x03A5, 0x0308, 0x03AB}, {0x03A5, 0x0314, 0x1F59}, {0x03A5, 0x0340, 0x1FEA},
{0x03A5, 0x0341, 0x038E}, {0x03A9, 0x0300, 0x1FFA}, {0x03A9, 0x0301, 0x038F}, {0x03A9, 0x0313, 0x1F68},
{0x03A9, 0x0314, 0x1F69}, {0x03A9, 0x0340, 0x1FFA}, {0x03A9, 0x0341, 0x038F}, {0x03A9, 0x0343, 0x1F68},
{0x03A9, 0x0345, 0x1FFC}, {0x03AC, 0x0345, 0x1FB4}, {0x03AE, 0x0345, 0x1FC4}, {0x03B1, 0x0300, 0x1F70},
{0x03B1, 0x0301, 0x03AC}, {0x03B1, 0x0304, 0x1FB1}, {0x03B1, 0x0306, 0x1FB0}, {0x03B1, 0x0313, 0x1F00},
{0x03B1, 0x0314, 0x1F01}, {0x03B1, 0x0340, 0x1F70}, {0x03B1, 0x0341, 0x03AC}, {0x03B1, 0x0342, 0x1FB6},
{0x03B1, 0x0343, 0x1F00}, {0x03B1, 0x0345, 0x1FB3}, {0x03B5, 0x0300, 0x1F72}, {0x03B5, 0x0301, 0x03AD},
{0x03B5, 0x0313, 0x1F10}, {0x03B5, 0x0314, 0x1F11}, {0x03B5, 0x0340, 0x1F72}, {0x03B5, 0x0341, 0x03AD},
{0x03B5, 0x0343, 0x1F10}, {0x03B7, 0x0300, 0x1F74}, {0x03B7, 0x0301, 0x03AE}, {0x03B7, 0x0313, 0x1F20},
{0x03B7, 0x0314, 0x1F21}, {0x03B7, 0x0340, 0x1F74}, {0x03B7, 0x0341, 0x03AE}, {0x03B7, 0x0342, 0x1FC6},
{0x03B7, 0x0343, 0x1F20}, {0x03B7, 0x0345, 0x1FC3}, {0x03B9, 0x0300, 0x1F76}, {0x03B9, 0x0301, 0x03AF},
{0x03B9, 0x0304, 0x1FD1}, {0x03B9, 0x0306, 0x1FD0}, {0x03B9, 0x0308, 0x03CA}, {0x03B9, 0x0313, 0x1F30},
{0x03B9, 0x0314, 0x1F31}, {0x03B9, 0x0340, 0x1F76}, {0x03B9, 0x0341, 0x03AF}, {0x03B9, 0x0342, 0x1FD6},
{0x03B9, 0x0343, 0x1F30}, {0x03B9, 0x0344, 0x0390}, {0x03BF, 0x0300, 0x1F78}, {0x03BF, 0x0301, 0x03CC},
{0x03BF, 0x0313, 0x1F40}, {0x03BF, 0x0314, 0x1F41}, {0x03BF, 0x0340, 0x1F78}, {0x03BF, 0x0341, 0x03CC},
{0x03BF, 0x0343, 0x1F40}, {0x03C1, 0x0313, 0x1FE4}, {0x03C1, 0x0314, 0x1FE5}, {0x03C1, 0x0343, 0x1FE4},
{0x03C5, 0x0300, 0x1F7A}, {0x03C5, 0x0301, 0x03CD}, {0x03C5, 0x0304, 0x1FE1}, {0x03C5, 0x0306, 0x1FE0},
{0x03C5, 0x0308, 0x03CB}, {0x03C5, 0x0313, 0x1F50}, {0x03C5, 0x0314, 0x1F51}, {0x03C5, 0x0340, 0x1F7A},
{0x03C5, 0x0341, 0x03CD}, {0x03C5, 0x0342, 0x1FE6}, {0x03C5, 0x0343, 0x1F50}, {0x03C5, 0x0344, 0x03B0},
{0x03C9, 0x0300, 0x1F7C}, {0x03C9, 0x0301, 0x03CE}, {0x03C9, 0x0313, 0x1F60}, {0x03C9, 0x0314, 0x1F61},
{0x03C9, 0x0340, 0x1F7C}, {0x03C9, 0x0341, 0x03CE}, {0x03C9, 0x0342, 0x1FF6}, {0x03C9, 0x0343, 0x1F60},
{0x03C9, 0x0345, 0x1FF3}, {0x03CA, 0x0300, 0x1FD2}, {0x03CA, 0x0301, 0x0390}, {0x03CA, 0x0340, 0x1FD2},
{0x03CA, 0x0341, 0x0390}, {0x03CA, 0x0342, 0x1FD7}, {0x03CB, 0x0300, 0x1FE2}, {0x03CB, 0x0301, 0x03B0},
{0x03CB, 0x0340, 0x1FE2}, {0x03CB, 0x0341, 0x03B0}, {0x03CB, 0x0342, 0x1FE7}, {0x03CE, 0x0345, 0x1FF4},
{0x03D2, 0x0301, 0x03D3}, {0x03D2, 0x0308, 0x03D4}, {0x03D2, 0x0341, 0x03D3}, {0x0406, 0x0308, 0x0407},
{0x0410, 0x0306, 0x04D0}, {0x0410, 0x0308, 0x04D2}, {0x0413, 0x0301, 0x0403}, {0x0413, 0x0341, 0x0403},
{0x0415, 0x0300, 0x0400}, {0x0415, 0x0306, 0x04D6}, {0x0415, 0x0308, 0x0401}, {0x0415, 0x0340, 0x0400},
{0x0416, 0x0306, 0x04C1}, {0x0416, 0x0308, 0x04DC}, {0x0417, 0x0308, 0x04DE}, {0x0418, 0x0300, 0x040D},
{0x0418, 0x0304, 0x04E2}, {0x0418, 0x0306, 0x0419}, {0x0418, 0x0308, 0x04E4}, {0x0418, 0x0340, 0x040D},
{0x041A, 0x0301, 0x040C}, {0x041A, 0x0341, 0x040C}, {0x041E, 0x0308, 0x04E6}, {0x0423, 0x0304, 0x04EE},
{0x0423, 0x0306, 0x040E}, {0x0423, 0x0308, 0x04F0}, {0x0423, 0x030B, 0x04F2}, {0x0427, 0x0308, 0x04F4},
{0x042B, 0x0308, 0x04F8}, {0x042D, 0x0308, 0x04EC}, {0x0430, 0x0306, 0x04D1}, {0x0430, 0x0308, 0x04D3},
{0x0433, 0x0301, 0x0453}, {0x0433, 0x0341, 0x0453}, {0x0435, 0x0300, 0x0450}, {0x0435, 0x0306, 0x04D7},
{0x0435, 0x0308, 0x0451}, {0x0435, 0x0340, 0x0450}, {0x0436, 0x0306, 0x04C2}, {0x0436, 0x0308, 0x04DD},
{0x0437, 0x0308, 0x04DF}, {0x0438, 0x0300, 0x045D}, {0x0438, 0x0304, 0x04E3}, {0x0438, 0x0306, 0x0439},
{0x0438, 0x0308, 0x04E5}, {0x0438, 0x0340, 0x045D}, {0x043A, 0x0301, 0x045C}, {0x043A, 0x0341, 0x045C},
{0x043E, 0x0308, 0x04E7}, {0x0443, 0x0304, 0x04EF}, {0x0443, 0x0306, 0x045E}, {0x0443, 0x0308, 0x04F1},
{0x0443, 0x030B, 0x04F3}, {0x0447, 0x0308, 0x04F5}, {0x044B, 0x0308, 0x04F9}, {0x044D, 0x0308, 0x04ED},
{0x0456, 0x0308, 0x0457}, {0x0474, 0x030F, 0x0476}, {0x0475, 0x030F, 0x0477}, {0x04D8, 0x0308, 0x04DA},
{0x04D9, 0x0308, 0x04DB}, {0x04E8, 0x0308, 0x04EA}, {0x04E9, 0x0308, 0x04EB}, {0x1E36, 0x0304, 0x1E38},
{0x1E37, 0x0304, 0x1E39}, {0x1E5A, 0x0304, 0x1E5C}, {0x1E5B, 0x0304, 0x1E5D}, {0x1E60, 0x0323, 0x1E68},
{0x1E61, 0x0323, 0x1E69}, {0x1E62, 0x0307, 0x1E68}, {0x1E63, 0x0307, 0x1E69}, {0x1EA0, 0x0302, 0x1EAC},
{0x1EA0, 0x0306, 0x1EB6}, {0x1EA1, 0x0302, 0x1EAD}, {0x1EA1, 0x0306, 0x1EB7}, {0x1EB8, 0x0302, 0x1EC6},
{0x1EB9, 0x0302, 0x1EC7}, {0x1ECC, 0x0302, 0x1ED8}, {0x1ECC, 0x031B, 0x1EE2}, {0x1ECD, 0x0302, 0x1ED9},
{0x1ECD, 0x031B, 0x1EE3}, {0x1ECE, 0x031B, 0x1EDE}, {0x1ECF, 0x031B, 0x1EDF}, {0x1EE4, 0x031B, 0x1EF0},
{0x1EE5, 0x031B, 0x1EF1}, {0x1EE6, 0x031B, 0x1EEC}, {0x1EE7, 0x031B, 0x1EED},
};
static constexpr int kUtf8ComposeTableSize = sizeof(kUtf8ComposeTable) / sizeof(kUtf8ComposeTable[0]);
+15 -18
View File
@@ -397,37 +397,34 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
// Continue out of block with data set // Continue out of block with data set
} else if (fileStat.method == ZIP_METHOD_DEFLATED) { } else if (fileStat.method == ZIP_METHOD_DEFLATED) {
// Read out deflated content from file auto* fileReadBuffer = static_cast<uint8_t*>(malloc(1024));
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize)); if (!fileReadBuffer) {
if (deflatedData == nullptr) { LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
free(data); free(data);
return nullptr; return nullptr;
} }
const size_t dataRead = file.read(deflatedData, deflatedDataSize); ZipInflateCtx ctx;
ctx.file = &file;
ctx.fileRemaining = deflatedDataSize;
ctx.readBuf = fileReadBuffer;
ctx.readBufSize = 1024;
if (dataRead != deflatedDataSize) { if (!ctx.reader.init(true)) {
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead); LOG_ERR("ZIP", "Failed to init inflate reader");
free(deflatedData); free(fileReadBuffer);
free(data); free(data);
return nullptr; return nullptr;
} }
ctx.reader.setReadCallback(zipReadCallback);
bool success = false; if (!ctx.reader.read(data, inflatedDataSize)) {
{
InflateReader r;
r.init(false);
r.setSource(deflatedData, deflatedDataSize);
success = r.read(data, inflatedDataSize);
}
free(deflatedData);
if (!success) {
LOG_ERR("ZIP", "Failed to inflate file"); LOG_ERR("ZIP", "Failed to inflate file");
free(fileReadBuffer);
free(data); free(data);
return nullptr; return nullptr;
} }
free(fileReadBuffer);
// Continue out of block with data set // Continue out of block with data set
} else { } else {
+21
View File
@@ -81,6 +81,27 @@ void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* m
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
} }
void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) {
// X3: a HALF fallback means the caller wants a clean base (e.g. the sleep
// cover, a full-screen swap from arbitrary prior content). Without this, the
// X3 grayscale base takes its gentle differential happy path and the prior
// home/reader frame ghosts through the soft aa_pre_bw_mid waveform. Forcing a
// resync makes displayGrayscaleBase clear first, matching displayBuffer(HALF).
// The reader's FAST path is deliberately left on the differential path so
// per-page grayscale stays cheap.
if (gpio.deviceIsX3() && fallback == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen);
}
void HalDisplay::preconditionGrayscale() { einkDisplay.preconditionGrayscale(); }
void HalDisplay::preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
einkDisplay.preconditionGrayscale(x, y, w, h);
}
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); } void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); }
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); } void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); }
+13
View File
@@ -47,6 +47,19 @@ class HalDisplay {
// Access to frame buffer // Access to frame buffer
uint8_t* getFrameBuffer() const; uint8_t* getFrameBuffer() const;
// X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed
// 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
// are written; no-op on X4. See EInkDisplay::preconditionGrayscale.
void preconditionGrayscale();
void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
// Display the framebuffer as the base frame for a grayscale overlay that
// follows. On X3, HALF fallback first requests a resync to match
// displayBuffer(HALF); FAST fallback keeps the OEM differential base waveform
// ("AA-pre-BW(mid)"). Other panels display normally with `fallback` mode.
void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false);
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer); void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer); void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
Submodule open-x4-sdk deleted from 26648d643a
+11 -5
View File
@@ -4,7 +4,7 @@ build_cache_dir = .cache
extra_configs = platformio.local.ini extra_configs = platformio.local.ini
[crosspoint] [crosspoint]
version = 1.3.0 version = 1.4.1
[base] [base]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
@@ -35,6 +35,8 @@ build_flags =
# Increase PNG scanline buffer to support up to 2048px wide images # Increase PNG scanline buffer to support up to 2048px wide images
# Default is (320*4+1)*2=2562, we need more for larger images # Default is (320*4+1)*2=2562, we need more for larger images
-DPNG_MAX_BUFFERED_PIXELS=16416 -DPNG_MAX_BUFFERED_PIXELS=16416
-DFREEINK_DEVICE_X4=1
-DFREEINK_DEVICE_X3=1
-Wno-bidi-chars -Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity -Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
-fno-exceptions -fno-exceptions
@@ -57,10 +59,14 @@ extra_scripts =
; Libraries ; Libraries
lib_deps = lib_deps =
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor BatteryMonitor=symlink://freeink-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager InputManager=symlink://freeink-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI
Icons=symlink://freeink-sdk/libs/assets/Icons
bblanchon/ArduinoJson @ 7.4.2 bblanchon/ArduinoJson @ 7.4.2
ricmoo/QRCode @ 0.0.1 ricmoo/QRCode @ 0.0.1
bitbank2/PNGdec @ 1.1.6 bitbank2/PNGdec @ 1.1.6
+8 -1
View File
@@ -6,6 +6,7 @@
#include <Serialization.h> #include <Serialization.h>
#include <cstring> #include <cstring>
#include <mutex>
#include <string> #include <string>
#include "I18nKeys.h" #include "I18nKeys.h"
@@ -96,6 +97,7 @@ uint8_t CrossPointSettings::sleepTimeoutEnumToMinutes(const uint8_t legacyValue)
} }
bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::saveToFile() const {
std::lock_guard<std::mutex> lock(_mutex);
Storage.mkdir("/.crosspoint"); Storage.mkdir("/.crosspoint");
return JsonSettingsIO::saveSettings(*this, SETTINGS_FILE_JSON); return JsonSettingsIO::saveSettings(*this, SETTINGS_FILE_JSON);
} }
@@ -106,7 +108,11 @@ bool CrossPointSettings::loadFromFile() {
String json = Storage.readFile(SETTINGS_FILE_JSON); String json = Storage.readFile(SETTINGS_FILE_JSON);
if (!json.isEmpty()) { if (!json.isEmpty()) {
bool resave = false; bool resave = false;
bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave); bool result;
{
std::lock_guard<std::mutex> lock(_mutex);
result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
}
if (result && resave) { if (result && resave) {
if (saveToFile()) { if (saveToFile()) {
LOG_DBG("CPS", "Resaved settings to update format"); LOG_DBG("CPS", "Resaved settings to update format");
@@ -166,6 +172,7 @@ bool CrossPointSettings::loadFromBinaryFile() {
if (!Storage.openFileForRead("CPS", SETTINGS_FILE_BIN, inputFile)) { if (!Storage.openFileForRead("CPS", SETTINGS_FILE_BIN, inputFile)) {
return false; return false;
} }
std::lock_guard<std::mutex> lock(_mutex);
uint8_t version; uint8_t version;
serialization::readPod(inputFile, version); serialization::readPod(inputFile, version);
+32 -2
View File
@@ -3,9 +3,12 @@
#include <cstdint> #include <cstdint>
#include <iosfwd> #include <iosfwd>
#include <mutex>
class CrossPointSettings { class CrossPointSettings {
private: private:
mutable std::mutex _mutex;
// Private constructor for singleton // Private constructor for singleton
CrossPointSettings() = default; CrossPointSettings() = default;
@@ -17,6 +20,10 @@ class CrossPointSettings {
CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete;
// Access the settings mutex for protecting multi-field reads/writes from other cores.
// Callers must not re-enter SETTINGS methods that lock _mutex while holding it.
std::mutex& getMutex() const { return _mutex; }
enum SLEEP_SCREEN_MODE { enum SLEEP_SCREEN_MODE {
DARK = 0, DARK = 0,
LIGHT = 1, LIGHT = 1,
@@ -65,6 +72,8 @@ class CrossPointSettings {
XTC_STATUS_BAR_MODE_COUNT XTC_STATUS_BAR_MODE_COUNT
}; };
enum STATUS_BAR_CLOCK_MODE { STATUS_BAR_CLOCK_HIDE = 0, STATUS_BAR_CLOCK_RIGHT = 1, STATUS_BAR_CLOCK_LEFT = 2 };
enum ORIENTATION { enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default) PORTRAIT = 0, // 480x800 logical coordinates (current default)
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
@@ -136,6 +145,17 @@ class CrossPointSettings {
// Short power button press actions // Short power button press actions
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT }; enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT };
// Long-press Confirm action while reading an EPUB. The setting cycles through these values.
// Persisted in settings.json by index: any new function (e.g. dictionary, bookmark) MUST use a
// value >= 2 and be appended at the END of the enumValues array in SettingsList.h, otherwise the
// stored indices shift and existing saves are silently misinterpreted.
enum LONG_PRESS_MENU_FUNCTION {
LP_MENU_KOSYNC = 0,
LP_MENU_DISABLED = 1,
LP_MENU_BOOKMARK = 2,
LONG_PRESS_MENU_FUNCTION_COUNT
};
// Hide battery percentage // Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
@@ -153,6 +173,13 @@ class CrossPointSettings {
// Image rendering in EPUB reader // Image rendering in EPUB reader
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT }; enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
// Text anti-aliasing mode for 2-bit fonts. FULL runs the grayscale LUT
// passes after the BW page render (extra refresh per page turn). FAST
// approximates AA inside the single BW pass by dithering partial-coverage
// glyph pixels (GfxRenderer fast anti-aliasing flag). Value 1 must stay
// FULL: it was the "on" value of the old on/off toggle in saved settings.
enum TEXT_ANTIALIASING { TEXT_AA_OFF = 0, TEXT_AA_FULL = 1, TEXT_AA_FAST = 2, TEXT_ANTIALIASING_COUNT };
enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT }; enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT };
enum QUICK_RESUME_SLEEP_SCREEN { enum QUICK_RESUME_SLEEP_SCREEN {
@@ -177,7 +204,7 @@ class CrossPointSettings {
uint8_t statusBarBattery = 1; uint8_t statusBarBattery = 1;
uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE; uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE;
// Clock display in status bar (X3 only, requires DS3231 RTC) // Clock display in status bar (X3 only, requires DS3231 RTC)
uint8_t statusBarClock = 0; uint8_t statusBarClock = STATUS_BAR_CLOCK_HIDE;
// Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t. // Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t.
// Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00. // Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00.
// Quarter-hour granularity supports oddball zones like Nepal (+5:45) and Chatham (+12:45). // Quarter-hour granularity supports oddball zones like Nepal (+5:45) and Chatham (+12:45).
@@ -189,7 +216,7 @@ class CrossPointSettings {
uint8_t clockHasBeenSynced = 0; uint8_t clockHasBeenSynced = 0;
// Text rendering settings // Text rendering settings
uint8_t extraParagraphSpacing = 1; uint8_t extraParagraphSpacing = 1;
uint8_t textAntiAliasing = 1; uint8_t textAntiAliasing = TEXT_AA_FULL;
// Short power button click behaviour // Short power button click behaviour
uint8_t shortPwrBtn = IGNORE; uint8_t shortPwrBtn = IGNORE;
// EPUB reading orientation settings // EPUB reading orientation settings
@@ -226,6 +253,9 @@ class CrossPointSettings {
uint8_t hideBatteryPercentage = HIDE_NEVER; uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior // Long-press page turn button behavior
uint8_t longPressButtonBehavior = OFF; uint8_t longPressButtonBehavior = OFF;
// Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values).
// Defaults to Disabled so shortcut-based bookmark toggling remains opt-in.
uint8_t longPressMenuFunction = LP_MENU_DISABLED;
// UI Theme // UI Theme
uint8_t uiTheme = LYRA; uint8_t uiTheme = LYRA;
// Sunlight fading compensation // Sunlight fading compensation
+4
View File
@@ -6,6 +6,7 @@
#include <Serialization.h> #include <Serialization.h>
#include <algorithm> #include <algorithm>
#include <mutex>
namespace { namespace {
constexpr uint8_t STATE_FILE_VERSION = 4; constexpr uint8_t STATE_FILE_VERSION = 4;
@@ -32,6 +33,7 @@ void CrossPointState::pushRecentSleep(uint16_t idx) {
} }
bool CrossPointState::saveToFile() const { bool CrossPointState::saveToFile() const {
std::lock_guard<std::mutex> lock(_mutex);
Storage.mkdir("/.crosspoint"); Storage.mkdir("/.crosspoint");
return JsonSettingsIO::saveState(*this, STATE_FILE_JSON); return JsonSettingsIO::saveState(*this, STATE_FILE_JSON);
} }
@@ -41,6 +43,7 @@ bool CrossPointState::loadFromFile() {
if (Storage.exists(STATE_FILE_JSON)) { if (Storage.exists(STATE_FILE_JSON)) {
String json = Storage.readFile(STATE_FILE_JSON); String json = Storage.readFile(STATE_FILE_JSON);
if (!json.isEmpty()) { if (!json.isEmpty()) {
std::lock_guard<std::mutex> lock(_mutex);
return JsonSettingsIO::loadState(*this, json.c_str()); return JsonSettingsIO::loadState(*this, json.c_str());
} }
} }
@@ -67,6 +70,7 @@ bool CrossPointState::loadFromBinaryFile() {
if (!Storage.openFileForRead("CPS", STATE_FILE_BIN, inputFile)) { if (!Storage.openFileForRead("CPS", STATE_FILE_BIN, inputFile)) {
return false; return false;
} }
std::lock_guard<std::mutex> lock(_mutex);
uint8_t version; uint8_t version;
serialization::readPod(inputFile, version); serialization::readPod(inputFile, version);
+6
View File
@@ -1,12 +1,18 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <mutex>
#include <string> #include <string>
class CrossPointState { class CrossPointState {
mutable std::mutex _mutex;
// Static instance // Static instance
static CrossPointState instance; static CrossPointState instance;
public: public:
// Access the state mutex for protecting multi-field reads/writes from other cores.
std::mutex& getMutex() const { return _mutex; }
static constexpr uint8_t SLEEP_RECENT_COUNT = 16; static constexpr uint8_t SLEEP_RECENT_COUNT = 16;
std::string openEpubPath; std::string openEpubPath;
+8 -4
View File
@@ -5,6 +5,7 @@
#include <Logging.h> #include <Logging.h>
#include <ObfuscationUtils.h> #include <ObfuscationUtils.h>
#include <algorithm>
#include <cstring> #include <cstring>
#include <string> #include <string>
@@ -153,10 +154,6 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
// Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders. // Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders.
doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN"; doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN";
// Language -- managed by LanguageSelectActivity, not in SettingsList.
// Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders.
doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN";
String json; String json;
serializeJson(doc, json); serializeJson(doc, json);
return Storage.writeFile(path, json); return Storage.writeFile(path, json);
@@ -345,6 +342,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
store.recentBooks.clear(); store.recentBooks.clear();
JsonArray arr = doc["books"].as<JsonArray>(); JsonArray arr = doc["books"].as<JsonArray>();
store.recentBooks.reserve(std::min(arr.size(), (size_t)10));
for (JsonObject obj : arr) { for (JsonObject obj : arr) {
if (store.getCount() >= 10) break; if (store.getCount() >= 10) break;
RecentBook book; RecentBook book;
@@ -423,6 +421,9 @@ bool JsonSettingsIO::saveBookmarks(const std::vector<BookmarkEntry>& bookmarks,
obj["xpath"] = bookmark.xpath; obj["xpath"] = bookmark.xpath;
obj["percentage"] = bookmark.percentage; obj["percentage"] = bookmark.percentage;
obj["summary"] = bookmark.summary; obj["summary"] = bookmark.summary;
obj["si"] = bookmark.computedSpineIndex;
obj["pc"] = bookmark.computedChapterPageCount;
obj["pp"] = bookmark.computedChapterProgress;
} }
String json; String json;
@@ -447,6 +448,9 @@ bool JsonSettingsIO::loadBookmarks(std::vector<BookmarkEntry>& bookmarks, const
bookmark.xpath = obj["xpath"] | std::string(""); bookmark.xpath = obj["xpath"] | std::string("");
bookmark.percentage = obj["percentage"] | static_cast<float>(0); bookmark.percentage = obj["percentage"] | static_cast<float>(0);
bookmark.summary = obj["summary"] | std::string(""); bookmark.summary = obj["summary"] | std::string("");
bookmark.computedSpineIndex = obj["si"] | static_cast<uint16_t>(0);
bookmark.computedChapterPageCount = obj["pc"] | static_cast<uint16_t>(0);
bookmark.computedChapterProgress = obj["pp"] | static_cast<uint16_t>(0);
} }
LOG_DBG("BKM", "Loaded %zu bookmarks from file", bookmarks.size()); LOG_DBG("BKM", "Loaded %zu bookmarks from file", bookmarks.size());
+21 -3
View File
@@ -1,7 +1,18 @@
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
bool MappedInputManager::isNavDirectionSwapped() const {
// Key the swap on the orientation the screen is *actually* rendered at, not the persisted reader
// setting. The reader (and its modal menus) render rotated, so navigation/labels flip there; the
// home and settings UI render in portrait, so they never flip even when a rotated reader is configured.
const auto orientation = renderer.getOrientation();
return SETTINGS.frontButtonFollowOrientation &&
(orientation == GfxRenderer::PortraitInverted || orientation == GfxRenderer::LandscapeCounterClockwise);
}
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
const auto sideLayout = SETTINGS.sideButtonLayout; const auto sideLayout = SETTINGS.sideButtonLayout;
@@ -49,6 +60,15 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
default: default:
return false; return false;
} }
case Button::NavNext:
// Logical "next item" navigation: side Down + front Right, with the control axis flipped in
// INVERTED / LANDSCAPE_CCW (frontButtonFollowOrientation) so it matches the rotated hint labels.
return isNavDirectionSwapped() ? (mapButton(Button::Up, fn) || mapButton(Button::Left, fn))
: (mapButton(Button::Down, fn) || mapButton(Button::Right, fn));
case Button::NavPrevious:
// Logical "previous item" navigation: side Up + front Left, axis-flipped in the same orientations.
return isNavDirectionSwapped() ? (mapButton(Button::Down, fn) || mapButton(Button::Right, fn))
: (mapButton(Button::Up, fn) || mapButton(Button::Left, fn));
} }
return false; return false;
@@ -69,9 +89,7 @@ unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const { const char* next) const {
// Swap previous/next labels to match the page turn direction swap in INVERTED and LANDSCAPE_CCW. // Swap previous/next labels to match the page turn direction swap in INVERTED and LANDSCAPE_CCW.
const bool swapLabels = const bool swapLabels = isNavDirectionSwapped();
SETTINGS.frontButtonFollowOrientation && (SETTINGS.orientation == CrossPointSettings::INVERTED ||
SETTINGS.orientation == CrossPointSettings::LANDSCAPE_CCW);
const char* leftLabel = swapLabels ? next : previous; const char* leftLabel = swapLabels ? next : previous;
const char* rightLabel = swapLabels ? previous : next; const char* rightLabel = swapLabels ? previous : next;
+16 -2
View File
@@ -2,9 +2,11 @@
#include <HalGPIO.h> #include <HalGPIO.h>
class GfxRenderer;
class MappedInputManager { class MappedInputManager {
public: public:
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward }; enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
struct Labels { struct Labels {
const char* btn1; const char* btn1;
@@ -13,7 +15,7 @@ class MappedInputManager {
const char* btn4; const char* btn4;
}; };
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {} MappedInputManager(HalGPIO& gpio, const GfxRenderer& renderer) : gpio(gpio), renderer(renderer) {}
void update() const { gpio.update(); } void update() const { gpio.update(); }
bool wasPressed(Button button) const; bool wasPressed(Button button) const;
@@ -26,8 +28,20 @@ class MappedInputManager {
// Returns the raw front button index that was pressed this frame (or -1 if none). // Returns the raw front button index that was pressed this frame (or -1 if none).
int getPressedFrontButton() const; int getPressedFrontButton() const;
// True when the control axis is flipped relative to the physical buttons: the user opted into
// orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED /
// LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting,
// so portrait UI (home, settings) never swaps while the reader and its menus do.
[[nodiscard]] bool isNavDirectionSwapped() const;
private: private:
HalGPIO& gpio; HalGPIO& gpio;
// Logical-to-physical button mapping depends on what the user is actually looking at: when the
// screen is rendered rotated, the directional buttons must flip to match. The renderer is the only
// authority on the *live* orientation (the reader rotates it and restores portrait on exit), so we
// read it here instead of CrossPointSettings.orientation, which is just the persisted reader
// preference and stays "rotated" even while portrait UI like home/settings is on screen.
const GfxRenderer& renderer;
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
}; };
+11 -4
View File
@@ -5,12 +5,16 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
namespace {
static uint8_t fontSizeEnumFromSettings() { static uint8_t fontSizeEnumFromSettings() {
uint8_t e = SETTINGS.fontSize; uint8_t e = SETTINGS.fontSize;
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
return e; return e;
} }
} // namespace
void SdCardFontSystem::begin(GfxRenderer& renderer) { void SdCardFontSystem::begin(GfxRenderer& renderer) {
registry_.discover(); registry_.discover();
@@ -30,10 +34,12 @@ void SdCardFontSystem::begin(GfxRenderer& renderer) {
} else { } else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName); LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
} }
} else { } else {
LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName); LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName);
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
} }
} }
@@ -72,12 +78,11 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily); LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily);
manager_.unloadAll(renderer); manager_.unloadAll(renderer);
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
return; return;
} }
auto sizes = family->availableSizes(); const auto* selected = family->findClosestReaderSize(sizeEnum);
uint8_t idx = sizeEnum; const uint8_t wantedPt = selected ? selected->pointSize : 0;
if (idx >= sizes.size()) idx = sizes.size() - 1;
uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx];
if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return; if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return;
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt, LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt,
sizeEnum, registryWasDirty ? " [registry dirty]" : ""); sizeEnum, registryWasDirty ? " [registry dirty]" : "");
@@ -94,10 +99,12 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
} else { } else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily); LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
} }
} else { } else {
LOG_DBG("SDFS", "SD font family not found: %s (clearing)", wantedFamily); LOG_DBG("SDFS", "SD font family not found: %s (clearing)", wantedFamily);
SETTINGS.sdFontFamilyName[0] = '\0'; SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
} }
} }
+18 -9
View File
@@ -104,8 +104,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
std::vector<SettingInfo> v = { std::vector<SettingInfo> v = {
// --- Display --- // --- Display ---
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER,
StrId::STR_COVER_CUSTOM, StrId::STR_QUICK_RESUME}, StrId::STR_COVER_CUSTOM, StrId::STR_NONE_OPT, StrId::STR_QUICK_RESUME},
"sleepScreen", StrId::STR_CAT_DISPLAY), "sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
@@ -151,13 +151,17 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, SettingInfo::Enum(
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
"orientation", StrId::STR_CAT_READER), {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_ORIENTATION_INVERTED, StrId::STR_LANDSCAPE_CCW},
"orientation", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing,
"extraParagraphSpacing", StrId::STR_CAT_READER), "extraParagraphSpacing", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", // Option order must match TEXT_ANTIALIASING values (popup index is
StrId::STR_CAT_READER), // stored directly): OFF=0, FULL=1 (legacy toggle "on"), FAST=2.
SettingInfo::Enum(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing,
{StrId::STR_STATE_OFF, StrId::STR_TEXT_AA_FULL, StrId::STR_TEXT_AA_FAST}, "textAntiAliasing",
StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
"imageRendering", StrId::STR_CAT_READER), "imageRendering", StrId::STR_CAT_READER),
@@ -171,12 +175,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
{StrId::STR_LONG_PRESS_BEHAVIOR_OFF, StrId::STR_LONG_PRESS_BEHAVIOR_SKIP, {StrId::STR_LONG_PRESS_BEHAVIOR_OFF, StrId::STR_LONG_PRESS_BEHAVIOR_SKIP,
StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION}, StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION},
"longPressButtonBehavior", StrId::STR_CAT_CONTROLS), "longPressButtonBehavior", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_LONG_PRESS_MENU, &CrossPointSettings::longPressMenuFunction,
{StrId::STR_KOSYNC, StrId::STR_DISABLED, StrId::STR_BOOKMARK_OPTION}, "longPressMenuFunction",
StrId::STR_CAT_CONTROLS),
SettingInfo::Enum( SettingInfo::Enum(
StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES}, {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES},
"shortPwrBtn", StrId::STR_CAT_CONTROLS), "shortPwrBtn", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack, SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack,
"pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS), "pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS),
// --- System --- // --- System ---
SettingInfo::Value( SettingInfo::Value(
StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeoutMinutes, StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeoutMinutes,
@@ -240,8 +248,9 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
StrId::STR_CUSTOMISE_STATUS_BAR), StrId::STR_CUSTOMISE_STATUS_BAR),
// Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset). // Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset).
// Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48. // Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48.
SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", SettingInfo::Enum(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock,
StrId::STR_CUSTOMISE_STATUS_BAR), {StrId::STR_HIDE, StrId::STR_DIR_LEFT, StrId::STR_DIR_RIGHT}, "statusBarClock",
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1}, SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1},
"clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR), "clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat, SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat,
+3
View File
@@ -6,6 +6,8 @@
#include <ObfuscationUtils.h> #include <ObfuscationUtils.h>
#include <Serialization.h> #include <Serialization.h>
#include <algorithm>
// Initialize the static instance // Initialize the static instance
WifiCredentialStore WifiCredentialStore::instance; WifiCredentialStore WifiCredentialStore::instance;
@@ -89,6 +91,7 @@ bool WifiCredentialStore::loadFromBinaryFile() {
serialization::readPod(file, count); serialization::readPod(file, count);
credentials.clear(); credentials.clear();
credentials.reserve(std::min<size_t>(count, MAX_NETWORKS));
for (uint8_t i = 0; i < count && i < MAX_NETWORKS; i++) { for (uint8_t i = 0; i < count && i < MAX_NETWORKS; i++) {
WifiCredential cred; WifiCredential cred;
serialization::readString(file, cred.ssid); serialization::readString(file, cred.ssid);
+14 -11
View File
@@ -1,5 +1,6 @@
#include "ActivityManager.h" #include "ActivityManager.h"
#include <FontCacheManager.h>
#include <HalPowerManager.h> #include <HalPowerManager.h>
#include <algorithm> #include <algorithm>
@@ -18,12 +19,15 @@
#include "settings/SettingsActivity.h" #include "settings/SettingsActivity.h"
#include "util/FullScreenMessageActivity.h" #include "util/FullScreenMessageActivity.h"
static portMUX_TYPE activityManagerSpinlock = portMUX_INITIALIZER_UNLOCKED;
void ActivityManager::begin() { void ActivityManager::begin() {
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender", xTaskCreatePinnedToCore(&renderTaskTrampoline, "ActivityManagerRender",
8192, // Stack size 8192, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
&renderTaskHandle // Task handle &renderTaskHandle, // Task handle
0 // Pin to core 0 (PRO_CPU)
); );
assert(renderTaskHandle != nullptr && "Failed to create render task"); assert(renderTaskHandle != nullptr && "Failed to create render task");
} }
@@ -45,10 +49,10 @@ void ActivityManager::renderTaskLoop() {
} }
// Notify any task blocked in requestUpdateAndWait() that the render is done. // Notify any task blocked in requestUpdateAndWait() that the render is done.
TaskHandle_t waiter = nullptr; TaskHandle_t waiter = nullptr;
taskENTER_CRITICAL(nullptr); taskENTER_CRITICAL(&activityManagerSpinlock);
waiter = waitingTaskHandle; waiter = waitingTaskHandle;
waitingTaskHandle = nullptr; waitingTaskHandle = nullptr;
taskEXIT_CRITICAL(nullptr); taskEXIT_CRITICAL(&activityManagerSpinlock);
if (waiter) { if (waiter) {
xTaskNotify(waiter, 1, eIncrement); xTaskNotify(waiter, 1, eIncrement);
} }
@@ -136,8 +140,7 @@ void ActivityManager::loop() {
} }
} }
if (requestedUpdate) { if (requestedUpdate.exchange(false)) {
requestedUpdate = false;
// Using direct notification to signal the render task to update // Using direct notification to signal the render task to update
// Increment counter so multiple rapid calls won't be lost // Increment counter so multiple rapid calls won't be lost
if (renderTaskHandle) { if (renderTaskHandle) {
@@ -279,7 +282,7 @@ void ActivityManager::requestUpdateAndWait() {
} }
// Atomic section to perform checks // Atomic section to perform checks
taskENTER_CRITICAL(nullptr); taskENTER_CRITICAL(&activityManagerSpinlock);
auto currTaskHandler = xTaskGetCurrentTaskHandle(); auto currTaskHandler = xTaskGetCurrentTaskHandle();
auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex); auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex);
bool isRenderTask = (currTaskHandler == renderTaskHandle); bool isRenderTask = (currTaskHandler == renderTaskHandle);
@@ -288,7 +291,7 @@ void ActivityManager::requestUpdateAndWait() {
if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) { if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) {
waitingTaskHandle = currTaskHandler; waitingTaskHandle = currTaskHandler;
} }
taskEXIT_CRITICAL(nullptr); taskEXIT_CRITICAL(&activityManagerSpinlock);
// Render task cannot call requestUpdateAndWait() or it will cause a deadlock // Render task cannot call requestUpdateAndWait() or it will cause a deadlock
assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()"); assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()");
+2 -1
View File
@@ -4,6 +4,7 @@
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <atomic>
#include <cassert> #include <cassert>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -63,7 +64,7 @@ class ActivityManager {
// Whether to trigger a render after the current loop() // Whether to trigger a render after the current loop()
// This variable must only be set by the main loop, to avoid race conditions // This variable must only be set by the main loop, to avoid race conditions
bool requestedUpdate = false; std::atomic<bool> requestedUpdate{false};
public: public:
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
+4
View File
@@ -43,6 +43,10 @@ struct PageResult {
struct ProgressChangeResult { struct ProgressChangeResult {
int spineIndex = 0; int spineIndex = 0;
int page = 0; int page = 0;
int totalPages = 0;
std::string xpath;
float percentage = 0.0f;
bool hasSavedProgress = false;
}; };
enum class NetworkMode; enum class NetworkMode;
+9 -3
View File
@@ -163,7 +163,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::FULL_REFRESH);
} }
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
@@ -218,7 +218,13 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(HalDisplay::HALF_REFRESH); if (hasGreyscale) {
// OEM grayscale pipeline base: use a full sleep-screen paint so the panel
// enters deep sleep from a clean B/W baseline before the gray nudge refresh.
renderer.displayGrayscaleBase(HalDisplay::FULL_REFRESH);
} else {
renderer.displayBuffer(HalDisplay::FULL_REFRESH);
}
if (hasGreyscale) { if (hasGreyscale) {
bitmap.rewindToData(); bitmap.rewindToData();
@@ -325,5 +331,5 @@ void SleepActivity::renderLastScreenSleepScreen() const {
void SleepActivity::renderBlankSleepScreen() const { void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen(); renderer.clearScreen();
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::FULL_REFRESH);
} }
+1 -1
View File
@@ -336,7 +336,7 @@ std::string getFileName(std::string filename) {
return filename.substr(0, pos); return filename.substr(0, pos);
} }
std::string getFileExtension(std::string filename) { std::string getFileExtension(const std::string& filename) {
if (filename.back() == '/') { if (filename.back() == '/') {
return ""; return "";
} }
+115
View File
@@ -0,0 +1,115 @@
#include "EndOfBookOptions.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include "CrossPointSettings.h"
#include "ReaderUtils.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/ButtonNavigator.h"
#include "util/NextBookFinder.h"
namespace {
// Display name without the file extension, mirroring the file browser rows
std::string displayName(const std::string& filename) {
const auto pos = filename.rfind('.');
return filename.substr(0, pos);
}
} // namespace
void EndOfBookOptions::loadOnce(const std::string& currentBookPath) {
if (isLoaded.load(std::memory_order_acquire)) {
return;
}
folder = FsHelpers::extractFolderPath(currentBookPath);
names = NextBookFinder::findNextBooks(currentBookPath, MAX_SUGGESTIONS);
selector = 0;
// Release-publish so the main task, which gates all access on isLoaded, never
// observes a partially built list
isLoaded.store(true, std::memory_order_release);
}
bool EndOfBookOptions::menuActive() const { return isLoaded.load(std::memory_order_acquire) && !names.empty(); }
std::string EndOfBookOptions::fullPath(const size_t index) const {
if (index >= names.size()) {
return {};
}
return folder == "/" ? "/" + names[index] : folder + "/" + names[index];
}
EndOfBookOptions::Action EndOfBookOptions::handleMenuInput(const MappedInputManager& input, std::string* openPath) {
if (input.wasReleased(MappedInputManager::Button::Confirm)) {
if (selector < static_cast<int>(names.size())) {
if (openPath) {
*openPath = fullPath(selector);
}
return Action::OpenBook;
}
return Action::GoHome; // "Home" entry selected
}
// Short-press Back returns to the last page; a long press falls through to the
// reader's own handler (file browser). Home is reached through the list's Home entry.
if (input.wasReleased(MappedInputManager::Button::Back) && input.getHeldTime() < ReaderUtils::GO_HOME_MS) {
return Action::LastPage;
}
// Selection movement on the standard list navigation buttons (side Up/Down plus front
// Left/Right, orientation swap included). It follows the reader's page-turn semantics
// (press-triggered by default, release-triggered when a long-press behavior is
// configured, same rule as ReaderUtils::detectPageTurn). This matters on entry: with
// press-triggered turns, the press that turned the final page already fired in the
// reader, and its release must not double-fire into this menu.
const bool usePress = SETTINGS.longPressButtonBehavior == CrossPointSettings::OFF;
const auto triggered = [&](const MappedInputManager::Button button) {
return usePress ? input.wasPressed(button) : input.wasReleased(button);
};
const int itemCount = static_cast<int>(names.size()) + 1; // + "Home" entry
if (triggered(MappedInputManager::Button::NavPrevious)) {
selector = ButtonNavigator::previousIndex(selector, itemCount); // wraps to the bottom
return Action::Redraw;
}
if (triggered(MappedInputManager::Button::NavNext)) {
selector = ButtonNavigator::nextIndex(selector, itemCount); // wraps to the top
return Action::Redraw;
}
return Action::None;
}
void EndOfBookOptions::render(GfxRenderer& renderer, const MappedInputManager& input) const {
const auto& metrics = UITheme::getInstance().getMetrics();
if (!menuActive()) {
// No suggestions: the historical plain end screen. 3/8 of the screen height matches
// the previous fixed position on the 480x800 panel and scales to other resolutions.
renderer.drawCenteredText(UI_12_FONT_ID, renderer.getScreenHeight() * 3 / 8, tr(STR_END_OF_BOOK), true,
EpdFontFamily::BOLD);
return;
}
// Suggestion menu: title, list (+ Home entry) and button hints. The hints are drawn at
// the physical front buttons, which is a logical side/top edge in the rotated
// orientations — lay out inside the safe area so nothing hides behind them. Vertical
// positions derive from the safe-area height and font line heights so other panel
// resolutions scale (review request on #2532).
const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false);
const int titleY = safe.y + safe.height / 8;
const int subtitleY = titleY + renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing;
const int listTop = subtitleY + renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing * 2;
UITheme::drawCenteredText(renderer, safe, UI_12_FONT_ID, titleY, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
UITheme::drawCenteredText(renderer, safe, UI_10_FONT_ID, subtitleY, tr(STR_EOB_CONTINUE_WITH));
const int listHeight = safe.y + safe.height - listTop - metrics.verticalSpacing;
GUI.drawList(renderer, Rect{safe.x, listTop, safe.width, listHeight}, static_cast<int>(names.size()) + 1, selector,
[this](const int index) {
return index < static_cast<int>(names.size()) ? displayName(names[index])
: std::string(tr(STR_EOB_HOME));
});
const auto labels = input.mapLabels(tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <atomic>
#include <string>
#include <vector>
class GfxRenderer;
class MappedInputManager;
// Shared End-of-Book next-book menu for the EPUB and XTC readers. Collects up to
// MAX_SUGGESTIONS sibling books once per reader session, handles the menu input, and
// draws the end screen. With no suggestions the end screen keeps its historical
// plain-title look and behavior.
class EndOfBookOptions {
public:
enum class Action { None, Redraw, OpenBook, GoHome, LastPage };
static constexpr size_t MAX_SUGGESTIONS = 3;
// Scans the book's folder for suggestions; no-op when already loaded. Call ONLY from
// the reader's render() (the render task, serialized by RenderLock) — the loaded flag
// is the release/acquire publication point that lets the main task read the finished
// list safely.
void loadOnce(const std::string& currentBookPath);
// True when the suggestion menu is showing and should own the reader's input.
bool menuActive() const;
// Menu input handling, following the standard list idiom: side Up/Down and front
// Left/Right move the selection (wrapping), Confirm opens it (or Home), and a short
// Back press returns to the last page of the book. Fills openPath when the result is
// OpenBook. Returns Action::None when nothing relevant was pressed; callers continue
// their normal input path (keeping long-press Back to the file browser working).
Action handleMenuInput(const MappedInputManager& input, std::string* openPath);
// Draws the full end screen (plain title, or the suggestion menu) onto a cleared buffer.
void render(GfxRenderer& renderer, const MappedInputManager& input) const;
private:
std::string folder;
// Written by the render task in loadOnce(), immutable afterwards; the main task only
// reads it after isLoaded is observed true (acquire), so no further locking is needed.
std::vector<std::string> names;
int selector = 0;
std::atomic<bool> isLoaded{false};
std::string fullPath(size_t index) const;
};
File diff suppressed because it is too large Load Diff
+39 -1
View File
@@ -5,6 +5,8 @@
#include <optional> #include <optional>
#include "BookmarkEntry.h"
#include "EndOfBookOptions.h"
#include "EpubReaderMenuActivity.h" #include "EpubReaderMenuActivity.h"
#include "ProgressMapper.h" #include "ProgressMapper.h"
#include "activities/Activity.h" #include "activities/Activity.h"
@@ -34,6 +36,9 @@ class EpubReaderActivity final : public Activity {
bool automaticPageTurnActive = false; bool automaticPageTurnActive = false;
bool showBookmarkMessage = false; bool showBookmarkMessage = false;
bool ignoreNextConfirmRelease = false; bool ignoreNextConfirmRelease = false;
bool currentPageBookmarked = false;
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
std::vector<BookmarkEntry> cachedBookmarks;
// Tracks whether this book is currently removed from Recent Books by the // Tracks whether this book is currently removed from Recent Books by the
// removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in). // removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in).
bool recentsEntryRemoved = false; bool recentsEntryRemoved = false;
@@ -41,6 +46,8 @@ class EpubReaderActivity final : public Activity {
// Set when the reader is left at end-of-book and SETTINGS.moveFinishedToReadFolder is on. // Set when the reader is left at end-of-book and SETTINGS.moveFinishedToReadFolder is on.
// Consumed in onExit() to relocate the finished book into /Read/. // Consumed in onExit() to relocate the finished book into /Read/.
bool pendingReadFolderMove = false; bool pendingReadFolderMove = false;
// Next-book suggestion menu for the End-of-Book screen
EndOfBookOptions endOfBookOptions;
// Footnote support // Footnote support
std::vector<FootnoteEntry> currentPageFootnotes; std::vector<FootnoteEntry> currentPageFootnotes;
@@ -55,15 +62,46 @@ class EpubReaderActivity final : public Activity {
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight, void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft); int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar() const; void renderStatusBar() const;
void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight); // Pages laid out per incremental-build pump: on the render path (catching up to the page
// being shown) and per loop() tick (background build of a large chapter). Kept small so a
// background build chunk never noticeably delays input or a pending render.
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
// How many pages to keep laid out ahead of the reader for a still-building section. A page
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
// -- a tiny buffer is enough. The background build stops once the watermark is this far
// ahead and resumes as the reader advances; building unbounded instead locked up input by
// monopolizing the RenderLock. A giant single-spine book therefore never finalizes its .bin
// in one sitting -- instant reopen comes from Section::suspendBuild() persisting the pages
// already laid out as a partial file on exit/sleep.
static constexpr int BUILD_WINDOW_AHEAD = 5;
// Show the indexing popup when an initial build must lay out more than this many pages up front
// (a deep resume/jump into a not-yet-built section), so it isn't a silent wait. Kept independent
// of the small look-ahead window so ordinary landings stay popup-free.
static constexpr int BUILD_POPUP_PAGE_THRESHOLD = 20;
// Also show the popup when first building a spine larger than this (uncompressed bytes): its
// whole HTML must be inflated before page 1 can lay out (the giant single-spine case), which is
// a multi-second wait. Normal chapters are well under this and stay popup-free.
static constexpr size_t BUILD_POPUP_BYTE_THRESHOLD = 96 * 1024;
// Remap the cached relative reading position once the section's real page count is known
// (used after a settings change re-paginates a chapter). Returns true if currentPage moved.
// No-op while the section is still building or when the pagination is unchanged (plain resume).
bool applyDeferredReposition();
bool saveProgress(int spineIndex, int currentPage, int pageCount); bool saveProgress(int spineIndex, int currentPage, int pageCount);
// Jump to a percentage of the book (0-100), mapping it to spine and page. // Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent); void jumpToPercent(int percent);
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
// Opens the reader menu for the current position (short-press Confirm)
void openReaderMenu();
// Returns true if sync acted (launched, or surfaced a save error); false if it was a no-op
// because no KOReader credentials are stored.
bool launchKOReaderSync();
void applyOrientation(uint8_t orientation); void applyOrientation(uint8_t orientation);
void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
void pageTurn(bool isForwardTurn); void pageTurn(bool isForwardTurn);
void loadCachedBookmarks();
void addBookmark(); void addBookmark();
void updateBookmarkFlag();
// Footnote navigation // Footnote navigation
void navigateToHref(const std::string& href, bool savePosition = false); void navigateToHref(const std::string& href, bool savePosition = false);
@@ -9,7 +9,6 @@
#include <algorithm> #include <algorithm>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ProgressMapper.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
@@ -39,14 +38,6 @@ void EpubReaderBookmarksActivity::onEnter() {
bookmarks.shrink_to_fit(); bookmarks.shrink_to_fit();
} else { } else {
JsonSettingsIO::loadBookmarks(bookmarks, json.c_str()); JsonSettingsIO::loadBookmarks(bookmarks, json.c_str());
// pre-compute bookmark page values for quicker rendering
for (auto& bookmark : bookmarks) {
CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer);
bookmark.computedSpineIndex = pos.spineIndex;
bookmark.computedChapterPageCount = pos.totalPages;
bookmark.computedChapterProgress = pos.pageNumber;
}
} }
} else { } else {
LOG_DBG("EPB", "No bookmark file found at %s, starting with empty bookmarks", path.c_str()); LOG_DBG("EPB", "No bookmark file found at %s, starting with empty bookmarks", path.c_str());
@@ -93,6 +84,14 @@ void EpubReaderBookmarksActivity::loop() {
selectorIndex--; selectorIndex--;
} }
if (bookmarks.empty()) {
ActivityResult result;
result.isCancelled = true;
setResult(std::move(result));
finish();
return;
}
requestUpdate(); requestUpdate();
confirmingDelete = DELETE_MODE_OFF; confirmingDelete = DELETE_MODE_OFF;
return; return;
@@ -108,8 +107,17 @@ void EpubReaderBookmarksActivity::loop() {
return; return;
} }
auto bookmark = bookmarks.at(selectorIndex); auto bookmark = bookmarks.at(selectorIndex);
CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer); ProgressChangeResult result{};
setResult(ProgressChangeResult{pos.spineIndex, pos.pageNumber}); result.xpath = bookmark.xpath;
result.percentage = bookmark.percentage;
result.hasSavedProgress = true;
if (bookmark.computedChapterPageCount > 0 && bookmark.computedChapterProgress < bookmark.computedChapterPageCount &&
bookmark.computedSpineIndex < epub->getSpineItemsCount()) {
result.spineIndex = bookmark.computedSpineIndex;
result.page = bookmark.computedChapterProgress;
result.totalPages = bookmark.computedChapterPageCount;
}
setResult(std::move(result));
finish(); finish();
return; return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
@@ -186,9 +194,12 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) {
auto bookmark = bookmarks.at(confirmingDelete >= DELETE_MODE_DISPLAY ? selectorIndex : index); auto bookmark = bookmarks.at(confirmingDelete >= DELETE_MODE_DISPLAY ? selectorIndex : index);
auto tocIndex = epub->getTocIndexForSpineIndex(bookmark.computedSpineIndex); auto tocIndex = epub->getTocIndexForSpineIndex(bookmark.computedSpineIndex);
auto tocTitle = (tocIndex >= 0) ? (epub->getTocItem(tocIndex)).title : tr(STR_UNNAMED); auto tocTitle = (tocIndex >= 0) ? (epub->getTocItem(tocIndex)).title : tr(STR_UNNAMED);
return std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - " + std::string subtitle = std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - ";
std::to_string(bookmark.computedChapterProgress + 1) + "/" + if (bookmark.computedChapterPageCount > 0) {
std::to_string(bookmark.computedChapterPageCount) + " - " + tocTitle; subtitle += std::to_string(bookmark.computedChapterProgress + 1) + "/" +
std::to_string(bookmark.computedChapterPageCount) + " - ";
}
return subtitle + tocTitle;
}; };
const auto getBookmarkIcon = [isPortrait](int index) { const auto getBookmarkIcon = [isPortrait](int index) {
// only enabled icon in portrait mode due to limitation with rotating icons for other orientations // only enabled icon in portrait mode due to limitation with rotating icons for other orientations
@@ -208,16 +219,13 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) {
getBookmarkTitle, getBookmarkSubtitle, getBookmarkIcon); getBookmarkTitle, getBookmarkSubtitle, getBookmarkIcon);
GUI.drawHelpText(renderer, Rect{contentX, pageHeight - hintGutterBottom, contentWidth, LINE_HEIGHT}, GUI.drawHelpText(renderer, Rect{contentX, pageHeight - hintGutterBottom, contentWidth, LINE_HEIGHT},
tr(STR_HOLD_CONFIRM_TO_DELETE)); tr(STR_HOLD_OPEN_TO_DELETE));
} }
} else {
GUI.drawHelpText(renderer, Rect{contentX, LINE_HEIGHT * 2, contentWidth, LINE_HEIGHT},
tr(STR_BOOKMARK_INSTRUCTIONS));
} }
const auto backLabel = confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_CANCEL) : tr(STR_BACK); const auto backLabel = confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_CANCEL) : tr(STR_BACK);
const auto confirmLabel = const auto confirmLabel =
bookmarks.size() > 0 ? (confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_DELETE) : tr(STR_OPEN)) : ""; bookmarks.size() > 0 ? (confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_DELETE) : tr(STR_SELECT)) : "";
const auto labels = mappedInput.mapLabels(backLabel, confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); const auto labels = mappedInput.mapLabels(backLabel, confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

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