151 Commits

Author SHA1 Message Date
cottongin
7819cf0f77 fix: correct book card highlight padding by increasing tile height
Instead of shrinking the highlight strip (which clipped author text),
increase homeCoverTileHeight from 310 to 318 for proper bottom padding.
Revert double-padding subtraction in bottomH/bottomSectionHeight.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---

### AI Usage

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

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

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

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

---

### AI Usage

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

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

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

- EpubReaderBookmarkSelectionActivity
- DictionaryWordSelectActivity
- DictionarySuggestionsActivity
- DictionaryDefinitionActivity
- LookedUpWordsActivity

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

Also fix stale conflict marker in EpubReaderMenuActivity.h.

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

## Summary

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

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

## Additional Context

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

---

### AI Usage

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

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

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

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

## Additional Context

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

---

### AI Usage

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

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

---------

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

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

Updating webserver.md documentation to align with 1.0.0 features

* **What changes are included?**

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

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

## Additional Context

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

Nothing comes to mind

---

### AI Usage

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

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

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

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

* **What changes are included?**

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

## Additional Context

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

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

One other note is that this only affects the Lyra Theme

---

### AI Usage

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

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

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

- Data Structures:

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

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

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

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

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

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

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

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

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

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

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

---

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

---------

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

---

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

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

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

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

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

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

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

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

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

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

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

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

---

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

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

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

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

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

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

---------

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

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

## Additional Context

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

### AI Usage

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

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

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

## Additional Context

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

---

### AI Usage

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

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

* Include dictionary as in-scope

## Additional Context

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

---

### AI Usage

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

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

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

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

**What changes are included?**

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

### AI Usage

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

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

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

---

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

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

---------

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

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

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

master branch:

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

This PR:

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

---

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 14:40:36 -05:00
Jake Kenneally
46c2109f1f perf: Improve large CSS files handling (#779)
Some checks failed
CI (build) / clang-format (push) Has been cancelled
CI (build) / cppcheck (push) Has been cancelled
CI (build) / build (push) Has been cancelled
CI (build) / Test Status (push) Has been cancelled
## Summary

Closes #766. Thank you for the help @bramschulting!

**What is the goal of this PR?** 
- First and foremost, fix issue #766.
- Through working on that, I realized the current CSS parsing/loading
code can be improved dramatically for large files and still had
additional performance improvements to be made, even with EPUBs with
small CSS.

**What changes are included?**
- Stream CSS parsing and reuse normalization buffers to cut allocations
- Add rule limits and selector validation to release rules and free up
memory when needed
- Skip CSS parsing/loading entirely when "Book's Embedded Style" is off

## Additional Context

- My test EPUB has been updated
[here](https://github.com/jdk2pq/css-test-epub) to include a very large
CSS file to test this out

---

### 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**_, Codex
2026-02-15 20:22:42 +03:00
cottongin
1383d75c84 feat: Add per-family font and per-language hyphenation build flags
Add OMIT_BOOKERLY, OMIT_NOTOSANS, OMIT_OPENDYSLEXIC flags to
selectively exclude font families, and OMIT_HYPH_DE/EN/ES/FR/IT/RU
flags to exclude individual hyphenation language tries.

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

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

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

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 20:50:03 -05:00
cottongin
c1dfe92ea3 Merge remote-tracking branch 'upstream/master' into mod/master 2026-02-14 19:53:57 -05:00
Xuan-Son Nguyen
5816ab2a47 feat: use pre-compressed HTML pages (#861)
Some checks failed
CI (build) / clang-format (push) Has been cancelled
CI (build) / cppcheck (push) Has been cancelled
CI (build) / build (push) Has been cancelled
CI (build) / Test Status (push) Has been cancelled
## Summary

Pre-compress the HTML file to save flash space. I'm using `gzip` because
it's supported everywhere (indeed, we are using the same optimization on
[llama.cpp server](https://github.com/ggml-org/llama.cpp), our HTML page
is huge 😅 ).

This free up ~40KB flash space.

Some users suggested using `brotli` which is known to further reduce 20%
in size, but it doesn't supported by firefox (only supports if served
via HTTPS), and some reverse proxy like nginx doesn't support it out of
the box (unrelated in this context, but just mention for completeness)

```
PR:
RAM:   [===       ]  31.0% (used 101700 bytes from 327680 bytes)
Flash: [==========]  95.5% (used 6259244 bytes from 6553600 bytes)

master:
RAM:   [===       ]  31.0% (used 101700 bytes from 327680 bytes)
Flash: [==========]  96.2% (used 6302416 bytes from 6553600 bytes)
```

---

### AI Usage

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

Did you use AI tools to help write this code? **PARTIALLY**, only the
python part
2026-02-14 18:49:39 +03:00
Max Stoller
2c0a105550 docs: Add requirement device be on when flashing (#877)
## Summary
Flashing requires the device to be unlocked/awake

## Additional Context

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

---

### AI Usage

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

Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
2026-02-14 18:46:27 +03:00
cottongin
82bfbd8fa6 merge upstream/master: logging pragma, screenshot retrieval, nbsp fix
Merge 3 upstream commits into mod/master:
- feat: Allow screenshot retrieval from device (#820)
- feat: Add central logging pragma (#843)
- fix: Account for nbsp character as non-breaking space (#757)

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

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

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

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

Also removes all debug instrumentation from the investigation.

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 11:12:27 -05:00
Jake Kenneally
6e51afb977 fix: Account for nbsp; character as non-breaking space (#757)
## Summary

Closes #743.

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

- Add back handling for HTML entities in expat. This was originally part
of the code that got removed
[here](https://github.com/crosspoint-reader/crosspoint-reader/pull/274)
- Handle `&nbsp;` characters to resolve issue #743 

**What changes are included?**

- Brought back HTML entity table from previous commit and refactored it
to use a static const char * table with linear lookup to reduce heap
allocations.
- Used `XML_SetDefaultHandlerExpand` in expat to parse out the entities
correctly, without needing them defined in DOCTYPE
- Added handling for `&nbsp;` so that the text stays together and
doesn't break onto a new line with text separated by an `&nbsp;`

## Additional Context

- This supersedes [this
PR](https://github.com/crosspoint-reader/crosspoint-reader/pull/751)
that simply handled `nbsp;` as whitespace. Instead, we want that
character to serve its true purpose and affect the line-breaking
algorithm.
- Updated my test EPUB [here](https://github.com/jdk2pq/css-test-epub)
with `&nbsp;` characters examples at the end of the book

---

### 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**_, Claude Code
2026-02-13 15:46:46 +01:00
jpirnay
cb24947477 feat: Add central logging pragma (#843)
## Summary

* Definition and use of a central LOG function, that can later be
extended or completely be removed (for public use where debugging
information may not be required) to save flash by suppressing the
-DENABLE_SERIAL_LOG like in the slim branch

* **What changes are included?**

## Additional Context
* By using the central logger the usual:
```
#include <HardwareSerial.h>
...
  Serial.printf("[%lu] [WCS] Obfuscating/deobfuscating %zu bytes\n", millis(), data.size());
```
would then become
```
#include <Logging.h>
...
  LOG_DBG("WCS", "Obfuscating/deobfuscating %zu bytes", data.size());
```
You do have ``LOG_DBG`` for debug messages, ``LOG_ERR`` for error
messages and ``LOG_INF`` for informational messages. Depending on the
verbosity level defined (see below) soe of these message types will be
suppressed/not-compiled.

* The normal compilation (default) will create a firmware.elf file of
42.194.356 bytes, the same code via slim will create 42.024.048 bytes -
170.308 bytes less
* Firmware.bin : 6.469.984 bytes for default, 6.418.672 bytes for slim -
51.312 bytes less


### AI Usage

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

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

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-02-13 12:16:39 +01:00
cottongin
31878a77bc feat: Add mod build environment with version + git hash
Add `env:mod` PlatformIO environment that sets CROSSPOINT_VERSION to
"{version}-mod+{git_hash}" via a pre-build script. Usage: `pio run -e mod -t upload`

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 19:36:14 -05:00
jpirnay
7a385d78a4 feat: Allow screenshot retrieval from device (#820)
## Summary

* Add a small loop in main to be able to receive external commands,
currently being sent via the debugging_monitor
* Implemented command: cmd:SCREENSHOT sends the currently displayed
screen to the monitor, which will then store it to screenshot.bmp

## Additional Context

I was getting annoyed with taking tilted/unsharp photos of the device
screen, so I added the ability to press Enter during the monitor
execution and type SCREENSHOT to send a command. Could be extended in
the future

[screenshot.bmp](https://github.com/user-attachments/files/25213230/screenshot.bmp)

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-13 02:31:15 +03:00
cottongin
905f694576 prerender book covers and thumbnails when opening a book for the first time
Moves cover/thumbnail generation from lazy (Home screen, Sleep screen) into
each reader activity's onEnter(). On first open, generates all needed BMPs
(cover, cover_crop, thumbnails for all theme heights) with a "Preparing
book..." progress popup. Subsequent opens skip instantly when files exist.

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 11:54:40 -05:00
Xuan Son Nguyen
73cd05827a move IDLE_POWER_SAVING_MS 2026-02-12 13:19:37 +01:00
Xuan Son Nguyen
ea32ba0f8d add HalPowerManager 2026-02-12 13:12:13 +01:00
Xuan Son Nguyen
f7b1113819 Merge branch 'master' into xsn/idle_cpu_freq 2026-02-12 11:37:32 +01:00
Xuan Son Nguyen
228a1cb511 rm test 2026-02-12 11:37:12 +01:00
Xuan-Son Nguyen
0991782fb4 feat: more power saving on idle (#801)
Some checks failed
CI (build) / clang-format (push) Has been cancelled
CI (build) / cppcheck (push) Has been cancelled
CI (build) / build (push) Has been cancelled
CI (build) / Test Status (push) Has been cancelled
## Summary

This PR extends the delay in main loop from 10ms to 50ms after the
device is idle for a while. This translates to extended battery life in
a longer period (see testing section above), while not hurting too much
the user experience.

With the help from [this
patch](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage),
I was able to measure the CPU usage on idle:

```
PR:
[20017] [MEM] Free: 150188 bytes, Total: 232092 bytes, Min Free: 150092 bytes
[20017] [IDLE] Idle time: 99.62% (CPU load: 0.38%)
[30042] [MEM] Free: 150188 bytes, Total: 232092 bytes, Min Free: 150092 bytes
[30042] [IDLE] Idle time: 99.63% (CPU load: 0.37%)
[40067] [MEM] Free: 150188 bytes, Total: 232092 bytes, Min Free: 150092 bytes
[40067] [IDLE] Idle time: 99.62% (CPU load: 0.38%)

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

While this is a x3.8 reduce in CPU usage, it doesn't translate to the
same amount of battery life extension in real life. The reasons are:
1. The CPU is not shut down completely
2. freeRTOS tick is still running (however, I planned to experiment with
tickless functionality)
3. Current leakage to other components, for example: voltage dividers,
eink screen, SD card, etc

A note on
[light-sleep](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/system/sleep_modes.html)
functionality: it is not possible in our use case because:
- Light-sleep for 50ms introduce too much overhead on wake up, it has
negative effect on battery life
- Light-sleep for longer period doesn't work because the ADC GPIO
buttons cannot be used as wake up source

## Testing (duration = 6 hrs)

To test this, I patched the `CrossPointSettings::getSleepTimeoutMs()` to
always returns a timeout of 6 hrs. This allow me to leave the device
idle for 6 hrs straight.

- On master branch, 6 hrs costs 26% battery life (100% --> 74%), meaning
battery life is ~23 hrs
- With this PR, 6 hrs costs 20% battery life (100% --> 80%), meaning
battery life is ~30 hrs

So in theory, this extends the battery by about 7 hrs. Even with some
error margin added, I think 3 hrs increase is possible with a normal
usage setup (i.e. only read ebooks, no wifi)

## Additional Context

Would appreciate if someone can test this with an oscilloscope.

---

### AI Usage

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

Did you use AI tools to help write this code? **NO**
2026-02-12 09:49:05 +01:00
jpirnay
3ae1007cbe fix: chore: make all debug messages uniform (#825)
## Summary

* Unify all serial port debug messages

## Additional Context

* All messages sent to the serial port now follow the "[timestamp]
[origin] payload" format (notable exception framework messages)

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-11 16:25:17 +01:00
Jonas Diemer
efb9b72e64 fix: Show "Back" in file browser if not in root, "Home" otherwise. (#822)
## Summary

Show "Back" in file browser if not in root, "Home" otherwise.

---

### AI Usage

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

Did you use AI tools to help write this code? YES
2026-02-11 16:44:10 +03:00
Dave Allie
4a210823a8 fix: Manually trigger GPIO update in File Browser mode (#819)
## Summary

* Manually trigger GPIO update in File Browser mode
* Previously just assumed that the GPIO data would update automatically
(presumably via yield), the data is currently updated in the main loop
(and now here as well during the middle of the processing loop).
* This allows the back button to be correctly detected instead of only
being checked once every 100ms or so for the button state.

## Additional Context

* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/579

---

### AI Usage

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

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


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

## Summary by CodeRabbit

* **Bug Fixes**
* Enhanced input state detection in the web server interface for more
responsive and accurate user command recognition during high-frequency
operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-11 13:42:37 +03:00
Xuan Son Nguyen
b72283d304 change cpu freq on idle 2026-02-10 23:27:45 +01:00
Jonas Diemer
f5b85f5ca1 fix: Reduce MIN_SIZE_FOR_POPUP to 10KB (#809)
Noticed that the Indexing... popup went missing despite 3-5 seconds
delay. Reducing to 10KB, so we get a popup for delays > ~2s.


### AI Usage

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

Did you use AI tools to help write this code? no
2026-02-10 16:15:23 +01:00
Xuan Son Nguyen
8cf226613b clang format 2026-02-10 14:19:16 +01:00
Jonas Diemer
7e93411f46 docs: Update USER_GUIDE.md (#817)
Added explanation how to recover from broken config/cache.



### AI Usage

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

Did you use AI tools to help write this code? no
2026-02-10 23:23:14 +11:00
Dave Allie
44452a42e9 fix: Prevent sleeping when in OPDS browser / downloading books (#818)
## Summary

* Prevent sleeping when in OPDS browser / downloading books

## Additional Context

* Raised in
https://github.com/crosspoint-reader/crosspoint-reader/discussions/673

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-10 22:56:22 +11:00
jpirnay
0c2df24f5c feat: Extend python debugging monitor functionality (keyword filter / suppress) (#810)
## Summary

* I needed the ability to filter and or suppress debug messages
containig certain keywords (eg [GFX] for render related stuff)
* Update of debugging_monitor.py script for development work

## Additional Context
```
usage: debugging_monitor.py [-h] [--baud BAUD] [--filter FILTER] [--suppress SUPPRESS] [port]

ESP32 Monitor with Graph

positional arguments:
  port                 Serial port

options:
  -h, --help           show this help message and exit
  --baud BAUD          Baud rate
  --filter FILTER      Only display lines containing this keyword (case-insensitive)
  --suppress SUPPRESS  Suppress lines containing this keyword (case-insensitive)
```
* plus a couple of platform specific defaults (port, pip style)
---

### AI Usage

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

Did you use AI tools to help write this code? NO
2026-02-10 22:07:56 +11:00
Jonas Diemer
3a12ca2725 docs: Update USER_GUIDE.md (#808)
Added info about optimizing EPUB.

### AI Usage

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

Did you use AI tools to help write this code? no
2026-02-10 22:04:32 +11:00
Xuan Son Nguyen
d4f25c44bf lower to 3 seconds 2026-02-10 11:31:28 +01:00
Eliz
98e6789626 feat: Connect to last wifi by default (#752)
## Summary

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

Use last connected network as default

* **What changes are included?**

- Refactor how an action type of Settings are handled
- Add a new System Settings option → Network
- Add the ability to forget a network in the Network Selection Screen
- Add the ability to Refresh network list
- Save the last connected network SSID
- Use the last connection whenever network is needed (OPDS, Koreader
sync, update etc)

## Additional Context

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


![IMG_6504](https://github.com/user-attachments/assets/e48fb013-b5c3-45c0-b284-e183e6fd5a68)

![IMG_6503](https://github.com/user-attachments/assets/78c4b6b6-4e7b-4656-b356-19d65ff6aa12)




https://github.com/user-attachments/assets/95bf34a8-44ce-4279-8cd8-f78524ce745b





---

### AI Usage

Did you use AI tools to help write this code? _** PARTIALLY: I wrote
most of it but I also used Gemini as assist.

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 20:41:44 +11:00
ThatCrispyToast
b5d28a3a9c feat: use natural sort in file browser (#722)
## Summary

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

Implement natural sort (e.g. "file1.txt, file2.txt, file10.txt" instead
of "file1.txt, file10.txt, file2.txt") for files in the
MyLibraryActivity menu

* **What changes are included?**

Modifies the `sortFileList` function under
`src/activities/home/MyLibraryActivity.cpp` to use natural sort as
opposed to lexicographical sort

## Additional Context

I wasn't entirely sure whether or not i should make this a configurable
option, but most file browsers and directory listing tools have this set
as an immutable default, so I opted against it.

* 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? _**NO**_
2026-02-10 01:09:24 +03:00
Kuanysh Bekkulov
bc12556da1 perf: Replace std::list with std::vector in TextBlock and ParsedText
Replace std::list with std::vector for the words, wordStyles,
wordXpos, and wordContinues containers in TextBlock and ParsedText.

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

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 11:52:55 -05:00
Xuan Son Nguyen
eb79b98f2b power saving on idle 2026-02-09 12:45:16 +01:00
harshit181
14ef625679 fix: issue if book href are absolute url and not relative to server (#741)
## Summary

fixing issue if book href are absolute url and not relative to the
server

## Additional Context

* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/632
* https://github.com/harshit181/RSSPub/issues/43

---

### 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-02-09 22:12:21 +11:00
Istiak Tridip
64d161e88b feat: unify navigation handling with system-wide continuous navigation (#600)
This PR unifies navigation handling & adds system-wide support for
continuous navigation.

## Summary
Holding down a navigation button now continuously advances through items
until the button is released. This removes the need for repeated
press-and-release actions and makes navigation faster and smoother,
especially in long menus or documents.

When page-based navigation is available, it will navigate through pages.
If not, it will progress through menu items or similar list-based UI
elements.

Additionally, this PR fixes inconsistencies in wrap-around behavior and
navigation index calculations.

Places where the navigation system was updated:
- Home Page
- Settings Pages
- My Library Page
- WiFi Selection Page
- OPDS Browser Page
- Keyboard
- File Transfer Page
- XTC Chapter Selector Page
- EPUB Chapter Selector Page

I’ve tested this on the device as much as possible and tried to match
the existing behavior. Please let me know if I missed anything. Thanks 🙏


![crosspoint](https://github.com/user-attachments/assets/6a3c7482-f45e-4a77-b156-721bb3b679e6)

---

Following the request from @osteotek and @daveallie for system-wide
support, the old PR (#379) has been closed in favor of this
consolidated, system-wide implementation.

---

### AI Usage

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

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-09 20:19:34 +11:00
cottongin
a85d5e627b .gitignore tweaks for mod fork 2026-02-09 04:15:00 -05:00
Fabio Barbon
e73bb3213f feat: Add Italian hyphenation support (#584)
## Summary

* **What is the goal of this PR?** Add Italian language hyphenation
support to improve text rendering for Italian books.
* **What changes are included?**

* Added Italian hyphenation trie (hyph-it.trie.h) generated from Typst's
hypher patterns
* Registered italianHyphenator in LanguageRegistry.cpp for language tag
it
  * Added Italian to the hyphenation evaluation test suite
  * Added Italian test data file with 5000 test cases

## Additional Context

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

---

### AI Usage

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

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

---------

Co-authored-by: drbourbon <fabio@MacBook-Air-di-Fabio.local>
2026-02-09 19:55:58 +11:00
Dave Allie
6202bfd651 Merge branch 'release/1.0.0' 2026-02-09 17:18:24 +11:00
Jake Kenneally
9e04eec072 feat: Add percentage support to CSS properties (#738)
## Summary
- Closes #730

**What is the goal of this PR?**
- Adds percentage-based value support to CSS properties that accept
percentages (padding, margin, text-indent)
 
**What changes are included?**
- Adds `Percent` as another CSS unit
- Passes the viewport width to `fromCssStyle` so that we can resolve
percentage-based values
- Adds a fallback of using an emspace for text-indent if we have an
unresolvable value for whatever reason

## Additional Context

- This was missed in my CSS support feature, and the fallback when we
encounter a percentage value is to use px instead. This means 5% (which
would be ~30px on the screen) turns into 5px. When percentages are used
in `text-indent`, this fallback behavior makes the indent look like a
single space character. Whoops! 😬

My test EPUB has been updated
[here](https://github.com/jdk2pq/css-test-epub) with percentage based
CSS values at the end of the book.

---

### 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**_, Claude Code
2026-02-09 08:32:45 +11:00
Jake Kenneally
9b04c2ec76 feat: Add percentage support to CSS properties (#738)
## Summary
- Closes #730

**What is the goal of this PR?**
- Adds percentage-based value support to CSS properties that accept
percentages (padding, margin, text-indent)
 
**What changes are included?**
- Adds `Percent` as another CSS unit
- Passes the viewport width to `fromCssStyle` so that we can resolve
percentage-based values
- Adds a fallback of using an emspace for text-indent if we have an
unresolvable value for whatever reason

## Additional Context

- This was missed in my CSS support feature, and the fallback when we
encounter a percentage value is to use px instead. This means 5% (which
would be ~30px on the screen) turns into 5px. When percentages are used
in `text-indent`, this fallback behavior makes the indent look like a
single space character. Whoops! 😬

My test EPUB has been updated
[here](https://github.com/jdk2pq/css-test-epub) with percentage based
CSS values at the end of the book.

---

### 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**_, Claude Code
2026-02-09 08:31:52 +11:00
Dave Allie
def1094411 Use GITHUB_REF_NAME over GITHUB_HEAD_REF in release candidate workflow 2026-02-09 08:24:10 +11:00
Dave Allie
ffddc2472b Use GITHUB_REF_NAME over GITHUB_HEAD_REF in release candidate workflow 2026-02-09 08:22:20 +11:00
Dave Allie
5765bbe821 Add release candidate workflow 2026-02-09 08:16:36 +11:00
Dave Allie
7538e55795 Move release candidate workflow to manual dispatch 2026-02-09 08:15:13 +11:00
Dave Allie
21e7d29286 fix: Allow OTA update from RC build to full release (#778)
## Summary

* Allow OTA update from RC build to full release
* If all the segments match, then also check if the current version
contains "-rc"

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-09 08:08:40 +11:00
Dave Allie
b4b028be3a fix: Allow OTA update from RC build to full release (#778)
## Summary

* Allow OTA update from RC build to full release
* If all the segments match, then also check if the current version
contains "-rc"

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-09 08:08:19 +11:00
Yaroslav
f34d7d2aac fix(ui): Add Back label in KOReader Sync screen (#770)
## Summary

- Remove duplicate Cancel option 
- Add Back label

<img width="435" height="613" alt="image"
src="https://github.com/user-attachments/assets/a3af4133-46fa-46e6-8360-a15dd7c4fe2a"
/>


## Result

<img width="575" height="431" alt="image"
src="https://github.com/user-attachments/assets/6ccdac89-43df-45bf-bcfa-3a7cc4bd88e4"
/>


---

### AI Usage

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

Closes #754
2026-02-09 07:51:51 +11:00
Justin Mitchell
71769490fb fix: Add EPUB 3 cover image detection (#760)
I had an epub that just showed a blank cover and wouldnt work for the
sleep screen either, turns out it was an epub3 and I guess we didn't
support that. Super simple fix here
2026-02-09 07:49:49 +11:00
Jesse Vincent
cda0a3f898 feat: A web editor for settings (#667)
## Summary

This is an updated version of @itsthisjustin's #346 that builds on
current master and also deduplicates the settings list so we don't have
two copies of the settings. In the Web UI, it should organize the
settings a little closer to what you see on device.

## Additional Context

I tested this live on device and it seems to play nicely for me. It's
re-based on master since master's settings stuff has moved somewhat
since the original PR and addresses the sole review comment #346 - it
also means that I don't need to manually key in the URL for my OPDS
server. :)

---

### AI Usage

My changes were implemented with Claude Opus 4.5 and Claude Code 2.1.25.
I don't know if @itsthisjustin's original work used AI assistance.

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-09 07:46:14 +11:00
Xuan-Son Nguyen
7f40c3f477 feat: add HalStorage (#656)
## Summary

Continue my changes to introduce the HAL infrastructure from
https://github.com/crosspoint-reader/crosspoint-reader/pull/522

This PR touches quite a lot of files, but most of them are just name
changing. It should not have any impacts to the end behavior.

## Additional Context

My plan is to firstly add this small shim layer, which sounds useless at
first, but then I'll implement an emulated driver which can be helpful
for testing and for development.

Currently, on my fork, I'm using a FS driver that allow "mounting" a
local directory from my computer to the device, much like the `-v` mount
option on docker. This allows me to quickly reset `.crosspoint`
directory if anything goes wrong. I plan to upstream this feature when
this PR get merged.

---

### AI Usage

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

Did you use AI tools to help write this code? NO
2026-02-09 07:29:14 +11:00
Jake Kenneally
5e52a46837 refactor: Rename "Embedded Style" to "Book's Embedded Style" (#746)
## Summary

**What is the goal of this PR?**
- Just a simple rename after feedback in #738

**What changes are included?**
- Renamed "Embedded Style" to "Book's Embedded Style" to more clearly
associate it with "Book's Style" option in "Paragraph Alignment"
settings

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO**_
2026-02-09 05:12:33 +11:00
Xuan-Son Nguyen
6909f127b4 perf: optimize drawPixel() (#748)
## Summary

Ref https://github.com/crosspoint-reader/crosspoint-reader/pull/737

This PR further reduce ~25ms from rendering time, testing inside the
Setting screen:

```
master:
[68440] [GFX] Time = 73 ms from clearScreen to displayBuffer

PR:
[97806] [GFX] Time = 47 ms from clearScreen to displayBuffer
```

And in extreme case (fill the entire screen with black or gray color):

```
master:
[1125] [   ] Test fillRectDither drawn in 327 ms
[1347] [   ] Test fillRect drawn in 222 ms

PR:
[1334] [   ] Test fillRectDither drawn in 225 ms
[1455] [   ] Test fillRect drawn in 121 ms
```

Note that
https://github.com/crosspoint-reader/crosspoint-reader/pull/737 is NOT
applied on top of this PR. But with 2 of them combined, it should reduce
from 47ms --> 42ms

## Details

This PR based on the fact that function calls are costly if the function
is small enough. For example, this simple call:

```
  int rotatedX = 0;
  int rotatedY = 0;
  rotateCoordinates(x, y, &rotatedX, &rotatedY);
```

Generated assembly code:

<img width="771" height="215" alt="image"
src="https://github.com/user-attachments/assets/37991659-3304-41c3-a3b2-fb967da53f82"
/>

This adds ~10 instructions just to prepare the registers prior to the
function call, plus some more instructions for the function's
epilogue/prologue. Inlining it removing all of these:

<img width="1471" height="832" alt="image"
src="https://github.com/user-attachments/assets/b67a22ee-93ba-4017-88ed-c973e28ec914"
/>

Of course, this optimization is not magic. It's only beneficial under 3
conditions:
- The function is small, not in size, but in terms of effective
instructions. For example, the `rotateCoordinates` is simply a jump
table, where each branch is just 3-4 inst
- The function has multiple input arguments, which requires some move to
put it onto the correct place
- The function is called very frequently (i.e. critical path)

---

### AI Usage

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

Did you use AI tools to help write this code? **NO**
2026-02-09 05:07:35 +11:00
Arthur Tazhitdinov
4f0a3aa4dd feat: wakeup target detection (#731)
## Summary

* If going to sleep was from the Reader view, wake up to the same book.
Otherwise, wakeup to the Home view
2026-02-09 05:07:32 +11:00
CaptainFrito
bb983d0ef4 fix: Scrolling page items calculation (#716)
## Summary

Fix for the page skip issue detected
https://github.com/crosspoint-reader/crosspoint-reader/pull/700#issuecomment-3856374323
by user @whyte-j

Skipping down on the last page now skips to the last item, and up on the
first page to the first item, rather than wrapping around the list in a
weird way.

## Additional Context

The calculation was outdated after several changes were added afterwards

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO**_
2026-02-09 05:07:23 +11:00
Xuan-Son Nguyen
b45eaf7ded feat: optimize fillRectDither (#737)
## Summary

This PR optimizes the `fillRectDither` function, making it as fast as a
normal `fillRect`

Testing code:

```cpp
  {
    auto start_t = millis();
    renderer.fillRectDither(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), Color::LightGray);
    auto elapsed = millis() - start_t;
    Serial.printf("[%lu] [   ] Test fillRectDither drawn in %lu ms\n", millis(), elapsed);
  }

  {
    auto start_t = millis();
    renderer.fillRect(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), true);
    auto elapsed = millis() - start_t;
    Serial.printf("[%lu] [   ] Test fillRect drawn in %lu ms\n", millis(), elapsed);
  }
```

Before:

```
[1125] [   ] Test fillRectDither drawn in 327 ms
[1347] [   ] Test fillRect drawn in 222 ms
```

After:

```
[1065] [ ] Test fillRectDither drawn in 238 ms
[1287] [ ] Test fillRect drawn in 222 ms
```

## Visual validation

Before:

<img width="415" height="216" alt="Screenshot 2026-02-07 at 01 04 19"
src="https://github.com/user-attachments/assets/5802dbba-187b-4d2b-a359-1318d3932d38"
/>

After:

<img width="420" height="191" alt="Screenshot 2026-02-07 at 01 36 30"
src="https://github.com/user-attachments/assets/3c3c8e14-3f3a-4205-be78-6ed771dcddf4"
/>

## Details

The original version is quite slow because it does quite a lot of
computations. A single pixel needs around 20 instructions just to know
if it's black or white:

<img width="1170" height="693" alt="Screenshot 2026-02-07 at 00 15 54"
src="https://github.com/user-attachments/assets/7c5a55e7-0598-4340-8b7b-17307d7921cb"
/>

With the new, templated and more light-weight approach, each pixel takes
only 3-4 instructions, the modulo operator is translated into bitwise
ops:

<img width="1175" height="682" alt="Screenshot 2026-02-07 at 01 47 51"
src="https://github.com/user-attachments/assets/4ec2cf74-6cc0-4b5b-87d5-831563ef164f"
/>

---

### AI Usage

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

Did you use AI tools to help write this code? **NO**
2026-02-09 05:07:13 +11:00
Xuan-Son Nguyen
a87eacc6ab perf: optimize drawPixel() (#748)
## Summary

Ref https://github.com/crosspoint-reader/crosspoint-reader/pull/737

This PR further reduce ~25ms from rendering time, testing inside the
Setting screen:

```
master:
[68440] [GFX] Time = 73 ms from clearScreen to displayBuffer

PR:
[97806] [GFX] Time = 47 ms from clearScreen to displayBuffer
```

And in extreme case (fill the entire screen with black or gray color):

```
master:
[1125] [   ] Test fillRectDither drawn in 327 ms
[1347] [   ] Test fillRect drawn in 222 ms

PR:
[1334] [   ] Test fillRectDither drawn in 225 ms
[1455] [   ] Test fillRect drawn in 121 ms
```

Note that
https://github.com/crosspoint-reader/crosspoint-reader/pull/737 is NOT
applied on top of this PR. But with 2 of them combined, it should reduce
from 47ms --> 42ms

## Details

This PR based on the fact that function calls are costly if the function
is small enough. For example, this simple call:

```
  int rotatedX = 0;
  int rotatedY = 0;
  rotateCoordinates(x, y, &rotatedX, &rotatedY);
```

Generated assembly code:

<img width="771" height="215" alt="image"
src="https://github.com/user-attachments/assets/37991659-3304-41c3-a3b2-fb967da53f82"
/>

This adds ~10 instructions just to prepare the registers prior to the
function call, plus some more instructions for the function's
epilogue/prologue. Inlining it removing all of these:

<img width="1471" height="832" alt="image"
src="https://github.com/user-attachments/assets/b67a22ee-93ba-4017-88ed-c973e28ec914"
/>

Of course, this optimization is not magic. It's only beneficial under 3
conditions:
- The function is small, not in size, but in terms of effective
instructions. For example, the `rotateCoordinates` is simply a jump
table, where each branch is just 3-4 inst
- The function has multiple input arguments, which requires some move to
put it onto the correct place
- The function is called very frequently (i.e. critical path)

---

### AI Usage

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

Did you use AI tools to help write this code? **NO**
2026-02-09 05:05:42 +11:00
Arthur Tazhitdinov
1caad578fc feat: wakeup target detection (#731)
## Summary

* If going to sleep was from the Reader view, wake up to the same book.
Otherwise, wakeup to the Home view
2026-02-09 05:01:30 +11:00
James Whyte
75b0ed7781 fix: increase lyra sideButtonHintsWidth to 30 (#727)
## Summary

Increase the width of Lyra's side button hints. It has been set to 30,
the same width as the classic theme.


Before:
<img width="457" height="742" alt="image"
src="https://github.com/user-attachments/assets/316e4679-fbf0-4f6e-b117-413075da1be2"
/>

After:
<img width="512" height="849" alt="image"
src="https://github.com/user-attachments/assets/3b0cf069-55ad-4d5a-a93c-4aeca3ff67f8"
/>



## Additional Context

Resolves
https://github.com/crosspoint-reader/crosspoint-reader/pull/700#issuecomment-3856983832

---

### AI Usage

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

Did you use AI tools to help write this code?
No
2026-02-09 05:00:09 +11:00
CaptainFrito
5b90b68e99 fix: Scrolling page items calculation (#716)
## Summary

Fix for the page skip issue detected
https://github.com/crosspoint-reader/crosspoint-reader/pull/700#issuecomment-3856374323
by user @whyte-j

Skipping down on the last page now skips to the last item, and up on the
first page to the first item, rather than wrapping around the list in a
weird way.

## Additional Context

The calculation was outdated after several changes were added afterwards

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO**_
2026-02-09 04:58:46 +11:00
Jake Kenneally
67ddd60fce refactor: Rename "Embedded Style" to "Book's Embedded Style" (#746)
## Summary

**What is the goal of this PR?**
- Just a simple rename after feedback in #738

**What changes are included?**
- Renamed "Embedded Style" to "Book's Embedded Style" to more clearly
associate it with "Book's Style" option in "Paragraph Alignment"
settings

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO**_
2026-02-08 20:34:06 +03:00
Xuan-Son Nguyen
76908d38e1 feat: optimize fillRectDither (#737)
## Summary

This PR optimizes the `fillRectDither` function, making it as fast as a
normal `fillRect`

Testing code:

```cpp
  {
    auto start_t = millis();
    renderer.fillRectDither(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), Color::LightGray);
    auto elapsed = millis() - start_t;
    Serial.printf("[%lu] [   ] Test fillRectDither drawn in %lu ms\n", millis(), elapsed);
  }

  {
    auto start_t = millis();
    renderer.fillRect(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), true);
    auto elapsed = millis() - start_t;
    Serial.printf("[%lu] [   ] Test fillRect drawn in %lu ms\n", millis(), elapsed);
  }
```

Before:

```
[1125] [   ] Test fillRectDither drawn in 327 ms
[1347] [   ] Test fillRect drawn in 222 ms
```

After:

```
[1065] [ ] Test fillRectDither drawn in 238 ms
[1287] [ ] Test fillRect drawn in 222 ms
```

## Visual validation

Before:

<img width="415" height="216" alt="Screenshot 2026-02-07 at 01 04 19"
src="https://github.com/user-attachments/assets/5802dbba-187b-4d2b-a359-1318d3932d38"
/>

After:

<img width="420" height="191" alt="Screenshot 2026-02-07 at 01 36 30"
src="https://github.com/user-attachments/assets/3c3c8e14-3f3a-4205-be78-6ed771dcddf4"
/>

## Details

The original version is quite slow because it does quite a lot of
computations. A single pixel needs around 20 instructions just to know
if it's black or white:

<img width="1170" height="693" alt="Screenshot 2026-02-07 at 00 15 54"
src="https://github.com/user-attachments/assets/7c5a55e7-0598-4340-8b7b-17307d7921cb"
/>

With the new, templated and more light-weight approach, each pixel takes
only 3-4 instructions, the modulo operator is translated into bitwise
ops:

<img width="1175" height="682" alt="Screenshot 2026-02-07 at 01 47 51"
src="https://github.com/user-attachments/assets/4ec2cf74-6cc0-4b5b-87d5-831563ef164f"
/>

---

### AI Usage

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

Did you use AI tools to help write this code? **NO**
2026-02-08 14:59:13 +03:00
Arthur Tazhitdinov
e6f5fa43e6 feat(ux): invert BACK button behavior in reader activities (#726)
## Summary

* Inverts back button behaviour while reading - short press to go home,
long press to open file browser

## Additional Context

* It seems counterintuitive that going into a book from home screen and
pressing back doesn’t take you back to the home screen. With the recent
books now displayed in the home view and a separate recents view, going
directly to the file browser is less necessary.
2026-02-07 10:17:00 -05:00
James Whyte
e7e31ac487 fix: increase lyra sideButtonHintsWidth to 30 (#727)
## Summary

Increase the width of Lyra's side button hints. It has been set to 30,
the same width as the classic theme.


Before:
<img width="457" height="742" alt="image"
src="https://github.com/user-attachments/assets/316e4679-fbf0-4f6e-b117-413075da1be2"
/>

After:
<img width="512" height="849" alt="image"
src="https://github.com/user-attachments/assets/3b0cf069-55ad-4d5a-a93c-4aeca3ff67f8"
/>



## Additional Context

Resolves
https://github.com/crosspoint-reader/crosspoint-reader/pull/700#issuecomment-3856983832

---

### AI Usage

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

Did you use AI tools to help write this code?
No
2026-02-07 10:15:41 -05:00
Jake Kenneally
47f3137dee fix: Remove separations after style changes (#720)
Closes #182. Closes #710. Closes #711.

## Summary

**What is the goal of this PR?**
- A longer-term, more robust fix for the issue with spurious spaces
appearing after style changes. Replaces solution from #694.

**What changes are included?**
- Add continuation flags to determine if to add a space after a word or
if the word connects to the previous word. Replaces simple solution that
only considered ending punctuation.
- Fixed an issue with greedy line-breaking algorithm where punctuation
could appear on the next line, separated from the word, if there was a
style change between the word and punctuation

---

### 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**_, Claude Code
2026-02-06 19:19:31 +11:00
CaptainFrito
d8632eae08 fix: Lag before displaying covers on home screen (#721)
## Summary

Reduce/fix the lag on the home screen before recent book covers are
rendered

## Additional Context

We were previously rendering the screen in two steps, delaying the
recent book covers render to avoid a lag before the screen loads.
In this PR, we are now doing that only if at least one book doesn't have
the cover thumbnail generated yet. If all thumbs are already generated,
we load and display them right away, with no lag.

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO **_
2026-02-06 19:19:27 +11:00
Jake Kenneally
9f78fd33e8 fix: Remove separations after style changes (#720)
Closes #182. Closes #710. Closes #711.

## Summary

**What is the goal of this PR?**
- A longer-term, more robust fix for the issue with spurious spaces
appearing after style changes. Replaces solution from #694.

**What changes are included?**
- Add continuation flags to determine if to add a space after a word or
if the word connects to the previous word. Replaces simple solution that
only considered ending punctuation.
- Fixed an issue with greedy line-breaking algorithm where punctuation
could appear on the next line, separated from the word, if there was a
style change between the word and punctuation

---

### 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**_, Claude Code
2026-02-06 19:10:37 +11:00
CaptainFrito
bd8132a260 fix: Lag before displaying covers on home screen (#721)
## Summary

Reduce/fix the lag on the home screen before recent book covers are
rendered

## Additional Context

We were previously rendering the screen in two steps, delaying the
recent book covers render to avoid a lag before the screen loads.
In this PR, we are now doing that only if at least one book doesn't have
the cover thumbnail generated yet. If all thumbs are already generated,
we load and display them right away, with no lag.

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO **_
2026-02-06 18:58:32 +11:00
Jake Kenneally
3223e85ea5 feat: Add Settings for toggling CSS on or off (#717)
Closes #712 

## Summary

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

- To add new settings for toggling on/off embedded CSS styles in the
reader. This gives more control and customization to the user over how
the ereader experience looks.

**What changes are included?**

- Added new "Embedded Style" option to the Reader settings
- Added new "Book's Style" option for "Paragraph Alignment"
- User's selected "Paragraph Alignment" will take precedence and
override the embedded CSS `text-align` property, _unless_ the user has
"Book's Style" set as their "Paragraph Alignment"

## Additional Context

![IMG_6336](https://github.com/user-attachments/assets/dff619ef-986d-465e-b352-73a76baae334)


https://github.com/user-attachments/assets/9e404b13-c7e0-41c7-9406-4715f389166a


Addresses feedback from the community about the new CSS feature:
https://github.com/crosspoint-reader/crosspoint-reader/pull/700

---

### 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**_, Claude Code
2026-02-06 18:50:24 +11:00
Jake Kenneally
f89ce514c8 feat: Add Settings for toggling CSS on or off (#717)
Closes #712 

## Summary

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

- To add new settings for toggling on/off embedded CSS styles in the
reader. This gives more control and customization to the user over how
the ereader experience looks.

**What changes are included?**

- Added new "Embedded Style" option to the Reader settings
- Added new "Book's Style" option for "Paragraph Alignment"
- User's selected "Paragraph Alignment" will take precedence and
override the embedded CSS `text-align` property, _unless_ the user has
"Book's Style" set as their "Paragraph Alignment"

## Additional Context

![IMG_6336](https://github.com/user-attachments/assets/dff619ef-986d-465e-b352-73a76baae334)


https://github.com/user-attachments/assets/9e404b13-c7e0-41c7-9406-4715f389166a


Addresses feedback from the community about the new CSS feature:
https://github.com/crosspoint-reader/crosspoint-reader/pull/700

---

### 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**_, Claude Code
2026-02-06 18:49:04 +11:00
Dave Allie
211153fcd5 Use GITHUB_HEAD_REF 2026-02-06 03:49:00 +11:00
Dave Allie
91777a9023 release: 1.0.0 2026-02-06 03:18:47 +11:00
Dave Allie
d8e813a78d chore: Swap logo (#699)
## Summary

* Direct swap of X with new logo for boot and sleep screens
* More to be done here in the future to make these screens look a little
nicer

## Additional Context

* The design comes straight from @lepislepis -
https://github.com/crosspoint-reader/crosspoint-reader/discussions/396#discussioncomment-15590508

---

### AI Usage

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

Did you use AI tools to help write this code? _**No
2026-02-06 02:50:01 +11:00
Luke Stein
c3b9bc38b9 feat: Add Chapter Progress Bar status bar option (#636)
## Summary
This pull request introduces a new "Chapter Progress Bar" mode to the
status bar, allowing users to track their progress within the current
chapter in addition to the existing book-level progress options. It also
unifies and increases the progress bar height for better visibility, and
updates the settings UI to support the new mode.

Closes #636

**Status Bar/Progress Bar Enhancements:**

* Added a new `CHAPTER_PROGRESS_BAR` mode to
`CrossPointSettings::STATUS_BAR_MODE`, and updated the settings UI to
allow users to select this mode.
[[1]](diffhunk://#diff-3af36372bb6233a83387a68091b5e0651c23585c7c0a95669ed893268ca709a8R34)
[[2]](diffhunk://#diff-c55df9ec3ade843be000ba463cb75aa3df27dc34620a56c248fc4cc4e917b34bL22-R23)
* Implemented `drawChapterProgressBar` in `ScreenComponents` and
integrated it into both EPUB and TXT reader activities, so the chapter
progress bar is displayed when the new mode is selected.
[[1]](diffhunk://#diff-be271778a942f7fab0d920acd73442512346ff811a4625c011275a7ca6be3a3eL51-R64)
[[2]](diffhunk://#diff-dd410cab3a363d78172706d2ad6591f327e9b5b05f314db405db31a667af03faL16-R20)
[[3]](diffhunk://#diff-82798dedbe135495e619d4aa27a4bef560c70c7663cf43148b67a26ddde45682R518-R525)
[[4]](diffhunk://#diff-471ba9d9eb65b1a8451d41246db2aa695a42ea4ae4762163adfda4c20fec0950R563-R567)
* Updated logic in EPUB and TXT reader activities to show the correct
progress bar, progress text, and battery indicator based on the selected
status bar mode, including the new chapter progress bar mode.
[[1]](diffhunk://#diff-82798dedbe135495e619d4aa27a4bef560c70c7663cf43148b67a26ddde45682R470-R481)
[[2]](diffhunk://#diff-82798dedbe135495e619d4aa27a4bef560c70c7663cf43148b67a26ddde45682L490-R503)
[[3]](diffhunk://#diff-471ba9d9eb65b1a8451d41246db2aa695a42ea4ae4762163adfda4c20fec0950R522-R533)
[[4]](diffhunk://#diff-471ba9d9eb65b1a8451d41246db2aa695a42ea4ae4762163adfda4c20fec0950L539-R548)

**UI/Visual Tweaks:**

* Increased the progress bar height from 4 to 6 pixels for improved
visibility, and refactored code to use the new constant.
[[1]](diffhunk://#diff-dd410cab3a363d78172706d2ad6591f327e9b5b05f314db405db31a667af03faL16-R20)
[[2]](diffhunk://#diff-82798dedbe135495e619d4aa27a4bef560c70c7663cf43148b67a26ddde45682L295-R295)
[[3]](diffhunk://#diff-471ba9d9eb65b1a8451d41246db2aa695a42ea4ae4762163adfda4c20fec0950L177-R177)

These changes collectively provide users with more granular progress
tracking options and a clearer visual indicator for reading progress.


## Additional Context



---

### AI Usage

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-02-06 02:49:38 +11:00
danoob
fb0af32ec0 feat: Move Sync feature to menu (#680)
## Summary

* **What is the goal of this PR?** 
Move the "Sync Progress" option from TOC (Chapter Selection) screen to
the Reader Menu, and fix use-after-free crashes related to callback
handling in activity lifecycle.

* **What changes are included?**
- Added "Sync Progress" as a menu item in `EpubReaderMenuActivity` (now
4 items: Go to Chapter, Sync Progress, Go Home, Delete Book Cache)
- Removed sync-related logic from `EpubReaderChapterSelectionActivity` -
TOC now only displays chapters
- Implemented `pendingGoHome` and `pendingSubactivityExit` flags in
`EpubReaderActivity` to safely handle activity destruction
- Fixed GO_HOME, DELETE_CACHE, and SYNC menu actions to use deferred
callbacks avoiding use-after-free

## Additional Context

* Root cause of crashes: callbacks like `onGoHome()` or `onCancel()`
invoked from activity handlers could destroy the current activity while
code was still executing, causing use-after-free and race conditions
with FreeRTOS display task.
* Solution: Deferred execution pattern - set flags and process them in
`loop()` after all nested activity loops have safely returned.
* Files changed: `EpubReaderMenuActivity.h`,
`EpubReaderActivity.h/.cpp`, `EpubReaderChapterSelectionActivity.h/.cpp`

---

### 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: danoooob <danoooob@example.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-06 02:04:38 +11:00
Jake Kenneally
cb4d86fec6 fix: prevent spurious spaces before attaching punctuation (#694)
Fixes issue #182

## Summary

**What is the goal of this PR?** 
When inline styles change mid-paragraph, words like periods, commas, and
quotes could end up as separate tokens. The justified text algorithm was
treating these as regular words, adding space before them.

**What changes are included?**

Now tracks which words are "attaching punctuation" (., , ! ? ; : " ' and
smart quotes) and excludes them from gap counting. These punctuation
marks attach directly to the preceding word without spacing.

## Additional Context

This is split out from code in #411 to address this comment
https://github.com/crosspoint-reader/crosspoint-reader/pull/411#discussion_r2751166631

---

### 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**_, Claude Code
2026-02-06 01:55:15 +11:00
Jonas Diemer
e94f056e8a fix: debug printf of cover name (#690)
## Summary

Minor fix

### AI Usage

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

Did you use AI tools to help write this code? no
2026-02-06 00:48:07 +11:00
James Whyte
20c5d8ccf8 feat: add shift lock to KeyboardEntryActivity (#513)
## Summary

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


https://github.com/user-attachments/assets/00973866-6b87-4d5b-a3bf-6f4f85a5e0a6

(Apologies for the graininess of the video - I was struggling to get it
below 10mb)

* **What changes are included?**
  * Relax shift disable criteria to include any character
* Add third shift option `LOCK` which is not disabled on character
entry.


## Additional Context

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

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO**_
2026-02-06 00:02:43 +11:00
Matthías Páll Gissurarson
d35bda8023 feat: rename and move in file manager (#630)
## Summary

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

This adds renaming and moving files to the File Manager

* **What changes are included?**

New `/move` and `/rename` endpoints, and corresponding modals and icons
added. Uses the `file.rename()` function, after sanity checking.

## Additional Context

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


Fixes #559, #661, #663. Only touches the File Manager, so low risk of
affecting other systems.

Simpler than #619, at the cost of not migrating the cache of renamed
books.

<img width="870" height="437" alt="image"
src="https://github.com/user-attachments/assets/73e0e750-dfc8-48e0-a7a6-9694470b7ded"
/>
<img width="575" height="318" alt="image"
src="https://github.com/user-attachments/assets/38c5fb19-c38a-436b-b3ad-75c1be7375ab"
/>
<img width="574" height="293" alt="image"
src="https://github.com/user-attachments/assets/1d2a2403-765d-473f-8c4f-c6968e9bbfeb"
/>


---

### 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**_

I used Codex for the implementation itself, and then carefully reviewed
the code myself. As this is a simple change and only to the webserver,
it is low risk.
2026-02-05 23:56:00 +11:00
Maik Allgöwer
d762325035 feat: Implement fix for sunlight fading issue (#603)
## Summary

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

The goal of this PR is to deliver a fix for or at least mitigate the
impact of the issue described in #561

* **What changes are included?**

This PR includes a new option "Sunlight Fading Fix" under "Settings ->
Display".

When set to ON, we will disable the displays analog supply voltage after
every update and turn it back on before the next update.

## Additional Context

* Until now, I was only able to do limited testing because of limited
sunlight at my location, but the fix seems to be working. I'll also
attach a pre-built binary based on 0.16.0 (current master) with the fix
applied to the linked ticket, as building this fix is a bit annoying
because the submodule open-x4-sdk also needs an update.
* [PR in
open-x4-sdk](https://github.com/open-x4-epaper/community-sdk/pull/15)
needs to be merged first, we also need to add another commit to this
here PR, updating this dependency.
* I decided to hide this behind a default-OFF option. While I'm not
really concerned that this fix might potentially damage the display,
someone more knowledgeable on E-Ink technology could maybe have a look
at this.
* There's a binary attached in the linked issue, if someone has the
required sunlight to test this in-depth.

---

### AI Usage

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

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

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 23:32:05 +11:00
Dave Allie
7f2b1a818e chore: Add PR title check on sync (#698)
## Summary

* Add PR title check on sync

## Additional Context

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-05 23:29:53 +11:00
Arthur Tazhitdinov
ddbe49f536 feat: Go To Position for epubs (#666)
## Summary

* Adds Go To % action in Epub Reader menu with slider style percent
selector

<img width="860" height="1147" alt="image"
src="https://github.com/user-attachments/assets/a38ecc71-429e-40e8-94ac-37fb1509dbd9"
/>

---

### 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: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-05 23:17:51 +11:00
Fabio Barbon
17fedd2a69 feat: Calibre Web Automated (CWA) koreader sync server support (#594)
## Summary

* **What is the goal of this PR?** Provide support to koreader sync
server embedded in the popular *Calibre Web Automated* self-hosted
digital library solution.
* **What changes are included?**

* Trivial addition of **HTTP Basic Auth (RFC 7617) header** to
`lib/KOReaderSync/KOReaderSyncClient.cpp`

## Additional Context

None

---

### AI Usage

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

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

---------

Co-authored-by: drbourbon <fabio@MacBook-Air-di-Fabio.local>
2026-02-05 23:10:08 +11:00
Dave Allie
768c2f8eed chore: Add CI check job to consolidate status (#696)
## Summary

* Add CI check job to consolidate status

## Additional Context

* As in the inline comment:
> This job is used as the PR required actions check, allows for changes
to other steps in the future without breaking PR requirements.

---

### AI Usage

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

Did you use AI tools to help write this code? No
2026-02-05 23:08:16 +11:00
Arthur Tazhitdinov
216dbc8ee3 chore: CI Build Summary - firmware stats, firmware artifact (#601)
## Summary

* adds to the Action Summary firmware RAM and Flash usage
* splits cppcheck, clang-format and build into separate jobs, more
apparent what had failed.
* attaches the built artifact (firmware.bin) to the Action Summary for
easier testing.


<img width="853" height="456" alt="image"
src="https://github.com/user-attachments/assets/61d21c3c-58c2-42f3-8d8e-4cbedcf193e9"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-05 23:00:18 +11:00
Arthur Tazhitdinov
ee987f07ff feat: quick rotate option in epub reader menu (#685)
## Summary

* adds rotation setting in epub reader menu, actual rotation happens on
going back
* improves button hint drawing to draw correctly in all orientations

<img width="860" height="1147" alt="image"
src="https://github.com/user-attachments/assets/91ceeca6-729f-4304-b68a-e412f6e2c9a7"
/>


---

### 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-02-05 22:53:35 +11:00
Егор Мартынов
23ecc52261 feat(settings): add "Cover + Custom" sleep screen mode (#582)
## Summary

Allows to fallback to custom sleep screens if the book does not have a
cover.

---

### 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: mrtnvgr <root@unixis.fun>
2026-02-05 22:46:14 +11:00
Jonas Diemer
edaf8fff9d fix: Artifacts on Thumb on Home Screen (#662)
## Summary

Use non-crop mode as expected for home thumb generation. I likely broke
this when I fixed the artifacts on the sleep screen.

---

### AI Usage

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

Did you use AI tools to help write this code? NO
2026-02-05 22:45:56 +11:00
GenesiaW
c8683340ab feat: holding back button while booting, boots to home screen as a mean of escaping boot loop (#587)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- Allows back button to be held to escape boot loops when reader
activity crashes and device attempts to boot to previous state
- Reduces the need of removing SD card and access to another device to
delete the `/.crosspoint/state.bin`

* **What changes are included?**
  - Back button can be held while booting to boot to home screen
  - Update of User Guide section to include this feature 

## Additional Context

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

---

### AI Usage

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

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

---------

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-02-05 22:45:09 +11:00
Dave Allie
5a9ee19eb8 docs: Add small SCOPE.md and GOVERNANCE.md documents (#640)
## Summary

* Additional documentation for CrossPoint in the way of a SCOPE.md and
GOVERNANCE.md document.
* These are intentionally pretty lightweight for now

---

### 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-02-05 22:38:42 +11:00
Arthur Tazhitdinov
c49a819939 feat: front button remapper (#664)
## Summary

* Custom remapper to create any variant of front button layout.

## Additional Context

* Included migration from previous frontlayout setting
* This will solve:
    *  https://github.com/crosspoint-reader/crosspoint-reader/issues/654
    * https://github.com/crosspoint-reader/crosspoint-reader/issues/652
    * https://github.com/crosspoint-reader/crosspoint-reader/issues/620
    * https://github.com/crosspoint-reader/crosspoint-reader/issues/468

<img width="860" height="1147" alt="image"
src="https://github.com/user-attachments/assets/457356ed-7a7d-4e1c-8683-e187a1df47c0"
/>



---

### 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-02-05 22:37:17 +11:00
CaptainFrito
bf87a7dc60 feat: UI themes, Lyra (#528)
## Summary

### What is the goal of this PR?

- Visual UI overhaul
- UI theme selection

### What changes are included?

- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation

![IMG_7649
Medium](https://github.com/user-attachments/assets/b516f5a9-2636-4565-acff-91a25b93b39b)
![IMG_7746
Medium](https://github.com/user-attachments/assets/def41810-ab6e-4952-b40f-b9ce7d62bea8)
![IMG_7651
Medium](https://github.com/user-attachments/assets/518a9a6d-107a-4be3-9533-43a2b64b944b)



## Additional Context

- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.

---

### AI Usage

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

Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 21:50:11 +11:00
Jake Kenneally
2cf799f45b feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary

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

- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment

## Additional Context

- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄

### Before

![IMG_6271](https://github.com/user-attachments/assets/dba7554d-efb6-4d13-88bc-8b83cd1fc615)

![IMG_6272](https://github.com/user-attachments/assets/61ba2de0-87c9-4f39-956f-013da4fe20a4)

### After

![IMG_6268](https://github.com/user-attachments/assets/ebe11796-cca9-4a46-b9c7-0709c7932818)

![IMG_6269](https://github.com/user-attachments/assets/e89c33dc-ff47-4bb7-855e-863fe44b3202)

---

### AI Usage

Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 21:28:10 +11:00
Xuan-Son Nguyen
db659f3ea2 fix: move http upload state to heap (#657)
## Summary

The main motivation behind this PR was because `uploadBuffer` is
statically allocated, but only used when web server is enabled. This
results in 4KB of memory sitting idle most of the time.

As expected, 4KB of initial RAM is freed with this PR:

```
master:
RAM:   [===       ]  32.5% (used 106508 bytes from 327680 bytes)

PR:
RAM:   [===       ]  31.2% (used 102276 bytes from 327680 bytes)
```

## Additional Context

This also highlights the importance of only using statically-allocated
buffer when absolutely needed (for example, if the component is active
most of the time). Otherwise, it makes more sense to tie the buffer's
life cycle to its activity.

---

### AI Usage

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

Did you use AI tools to help write this code? NO
2026-02-05 21:03:19 +11:00
Jake Kenneally
78d6e5931c fix: Correct debugging_monitor.py script instructions (#676)
## Summary

**What is the goal of this PR?**
- Minor correction to the `debugging_monitor.py` script instructions

**What changes are included?**
- `pyserial` should be installed, NOT `serial`, which is a [different
lib](https://pypi.org/project/serial/)
- Added macOS serial port

## Additional Context

- Just a minor docs update. I can confirm the debugging script is
working great on macOS

---

### AI Usage

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

Did you use AI tools to help write this code? _**< NO >**_
2026-02-04 00:33:20 +03:00
Luke Stein
dac11c3fdd fix: Correct instruction text to match actual button text (#672)
## Summary

* Instruction text says "Press OK to scan again" but button label is
actually "Connect" (not OK)
* Corrects instruction text

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-04 00:32:52 +03:00
Aaron Cunliffe
d403044f76 fix: Increase network SSID display length (#670)
## Rationale 

I have 2 wifi access points with almost identical names, just one has
`_EXT` at the end of it. With the current display limit of 13 characters
before adding ellipsis, I can't tell which is which.

Before device screenshot with masked SSIDs:
<img
src="https://github.com/user-attachments/assets/3c5cbbaa-b2f6-412f-b5a8-6278963bd0f2"
width="300">


## Summary

Adjusted displayed length from 13 characters to 30 in the Wifi selection
screen - I've left some space for potential proportional font changes in
the future

After image with masked SSIDs:
<img
src="https://github.com/user-attachments/assets/c5f0712b-bbd3-4eec-9820-4693fae90c9f"
width="300">

---

### AI Usage

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

Did you use AI tools to help write this code? _**< NO >**_
2026-02-03 18:24:23 +03:00
Aaron Cunliffe
f67c544e16 fix: webserver folder creation regex change (#653)
## Summary

Resolves #562 

Implements regex change to support valid characters discussed by
@daveallie in issue
[here](https://github.com/crosspoint-reader/crosspoint-reader/issues/562#issuecomment-3830809156).

Also rejects `.` and `..` as folder names which are invalid in FAT32 and
exFAT filesystems

## Additional Context
- Unsure on the wording for the alert, it feels overly explicit, but
that might be a good thing. Happy to change.

---

### 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-02-02 21:27:02 +11:00
Uri Tauber
e5c0ddc9fa feat: Debugging monitor script (#555)
## Summary

* **What is the goal of this PR?**
Add a debugging script to help developers monitor the ESP32 serial port
directly from a PC.

* **What changes are included?**
Added a new script: scripts/debugging_monitor.py

## Additional Context

While working on a new Crosspoint-Reader feature, it quickly became
clear that watching the ESP32 serial output without any visual cues was
inconvenient and easy to mess up.

This script improves the debugging experience by reading data from the
serial port and providing:

1. A timestamp prefix for every log line (instead of milliseconds since
power-up)
2. Color-coded output for different message types
3. A secondary window displaying a live graph of RAM usage, which is
especially useful for tracking the memory impact of new features

<img width="1916" height="1049" alt="Screenshot_20260126_183811"
src="https://github.com/user-attachments/assets/6291887f-ac17-43ac-9e43-f5dec8a7097e"
/>

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY >**_
I wrote the initial version of the script. Gemini was used to help add
the Matplotlib-based graphing and threading logic.
2026-02-01 22:53:20 +11:00
Arthur Tazhitdinov
b1dcb7733b fix: truncating chapter titles using UTF-8 safe function (#599)
## Summary

* Truncating chapter titles using utf8 safe functions (Cyrillic titles
were split mid codepoint)
* refactoring of lib/Utf8

---

### 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-02-01 22:23:48 +11:00
Arthur Tazhitdinov
0d82b03981 fix: don't wake up after USB connect (#644)
## Summary

* fixes problem that if short power button press is enabled, connecting
device to usb leads to waking up
2026-02-01 22:19:33 +11:00
Dave Allie
5a97334ace Revert "fix: don't wake up after USB connect" (#643)
Reverts crosspoint-reader/crosspoint-reader#576

Causing a boot loop on master
2026-02-01 21:35:25 +11:00
Gaspar Fabrega Ragni
4dd73a211a fix: custom sleep not showing image at index 0 (#639)
## Summary

* Fixing custom sleep behaviour where the first image in the /sleep
directory is not shown
* image at index 0 is not being rendered when more than 1 image is
stored in /sleep directory, because `APP_STATE.lastSleepImage` is always
0.

## Additional Context

* `APP_STATE.lastSleepImage` is reset to 0 when a epub is open, this
value is only used to compare it to the randomly selected one in
`renderCustomSleepScreen()` that should always be a valid index, since
the list of valid bmp images is colected from scratch. -> no need to
reset it and block image @ index 0 from being rendered

---

### AI Usage

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

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

Co-authored-by: Oyster <Oyster@home>
2026-02-01 19:22:52 +11:00
Thomas Foskolos
634f6279cb docs: Update USER_GUIDE.md (#625)
Add Greek as not supported language atm on the use guide
2026-02-01 19:21:36 +11:00
nscheung
11b2a59233 fix: Hide button hints in landscape CW mode (#637)
## Summary

* This change hides the button hints from overlapping chapter titles
when in landscape CW mode.

Before

![Before](https://github.com/user-attachments/assets/3015a9b3-3fa5-443b-a641-3e65841a6fbc)
After

![After](https://github.com/user-attachments/assets/011de15d-5ae6-429c-8f91-d8f37abe52d9)

## Additional Context

* I initially considered implementing an offset fix, but with potential
UI changes on the horizon, hiding the button hints appears to be the
simplest solution for now.

---

### 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-02-01 19:21:28 +11:00
Luke Stein
12c20bb09e fix: WiFi error screen text clarifications (#612)
## Summary

* Clarify strings on Wifi connection error screens
* I have confirmed on device that these are short enough not to overflow
screen margins

## Additional Context

* Several screens give duplicative text (e.g., header "Connection
Failed" with contents text "Connection failed") or slightly confusing
text (header "Forget Network?" with text "Remove saved password?")

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-01 19:19:23 +11:00
Arthur Tazhitdinov
6b7065b986 fix: don't wake up after USB connect (#576)
## Summary

* fixes problem that if short power button press is enabled, connecting
device to usb leads to waking up
2026-02-01 18:51:31 +11:00
Arthur Tazhitdinov
f4df513bf3 feat(ui): change popup logic (#442)
## Summary

* refactors Indexing popups into ScreenComponents (they had different
implementations in different files)
* removes Indexing popup for small chapters
* only show Indexing popup (without progress bar) for large chapters
(using same minimum file size condition as for progress bar before)

## Additional Context

* Having to show even single popup message and redraw the screen slows
down the flow significantly
* Testing results:
    * Opening large chapter with progress bar - 11 seconds
* Same chapter without progress bar, only single Indexing popup - 5
seconds

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY>**_
2026-02-01 18:41:24 +11:00
Jonas Diemer
f935b59a41 feat: Add reading menu and delete cache function (#433)
## Summary

* Adds a menu in the Epub reader
* The Chapter selection is moved there to pos 1 (so it can be reached by
double tapping the confirm button)
* A Go Home is there, too
* Most significantly, a function "Delete Book Cache" is added. This
returns to main (to avoid directly rebuilding cached items, eg. if this
is used to debug/develop other areas - and it's also easier ;))

Probably, the Sync function could now be moved from the Chapter
selection to this menu, too.

---

### 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-02-01 18:34:30 +11:00
250 changed files with 48379 additions and 20174 deletions

View File

@@ -1,10 +1,55 @@
name: CI name: CI (build)
'on':
on:
push: push:
branches: [master] branches: [master]
pull_request: pull_request:
permissions:
contents: read
jobs: jobs:
clang-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install clang-format-21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
- name: Run clang-format
run: |
PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix
git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
cppcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -19,19 +64,46 @@ jobs:
- name: Install PlatformIO Core - name: Install PlatformIO Core
run: pip install --upgrade platformio run: pip install --upgrade platformio
- name: Install clang-format-21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
- name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
- name: Build CrossPoint - name: Build CrossPoint
run: pio run run: |
set -euo pipefail
pio run | tee pio.log
- name: Extract firmware stats
run: |
set -euo pipefail
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## Firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload firmware.bin artifact
uses: actions/upload-artifact@v6
with:
name: firmware.bin
path: .pio/build/default/firmware.bin
if-no-files-found: error
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
# PR requirements.
test-status:
name: Test Status
needs:
- build
- clang-format
- cppcheck
if: always()
runs-on: ubuntu-latest
steps:
- name: Fail because needed jobs failed
# Fail if any job failed or was cancelled (skipped jobs are ok)
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1
- name: Success
run: exit 0

View File

@@ -1,6 +1,7 @@
name: "PR Formatting" name: "PR Formatting"
on: on:
pull_request:
pull_request_target: pull_request_target:
types: types:
- opened - opened

48
.github/workflows/release_candidate.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Compile Release Candidate
on:
workflow_dispatch:
jobs:
build-release-candidate:
if: startsWith(github.ref_name, 'release/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Extract env
run: |
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
- name: Build CrossPoint Release Candidate
env:
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
run: pio run -e gh_release_rc
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
path: |
.pio/build/gh_release_rc/bootloader.bin
.pio/build/gh_release_rc/firmware.bin
.pio/build/gh_release_rc/firmware.elf
.pio/build/gh_release_rc/firmware.map
.pio/build/gh_release_rc/partitions.bin

6
.gitignore vendored
View File

@@ -3,9 +3,15 @@
.DS_Store .DS_Store
.vscode .vscode
lib/EpdFont/fontsrc lib/EpdFont/fontsrc
lib/I18n/I18nStrings.cpp
*.generated.h *.generated.h
.vs .vs
build build
**/__pycache__/ **/__pycache__/
/compile_commands.json /compile_commands.json
/.cache /.cache
# mod
mod/*
.cursor/*
chat-summaries/*

38
GOVERNANCE.md Normal file
View File

@@ -0,0 +1,38 @@
# Project Governance & Community Principles
CrossPoint Reader is a community-driven, open-source project. Our goal is to provide a high-quality, open-source
firmware alternative for the Xteink X4 hardware. To keep this project productive and welcoming as we grow, we ask all
contributors to follow these principles.
### 1. The "Human First" Rule
Technical discussions can get heated, but they should never be personal.
- **Assume good intent:** We are all volunteers working on this in our free time. If a comment seems abrasive, assume
its a language barrier or a misunderstanding before taking offense.
- **Focus on the code, not the person:** Critique the implementation, the performance, or the UX. Never the intelligence
or character of the contributor.
- **Inflammatory language:** Personal attacks, trolling, or exclusionary language (based on race, gender, background,
etc.) are not welcome here and will be moderated.
### 2. A "Do-ocracy" with Guidance
CrossPoint thrives because people step up to build what they want to see.
- If you want a feature, the best way to get it is to start an
[Idea Discussion](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) or open a PR.
- If you want to report a bug, check for duplicates and create an
[Issue](https://github.com/crosspoint-reader/crosspoint-reader/issues).
- While we encourage experimentation, the maintainers reserve the right to guide the projects technical direction to
ensure stability on the ESP32-C3s constrained hardware.
- For more guidance on the scope of the project, see the [SCOPE.md](SCOPE.md) document.
### 3. Transparent Communication
To keep the project healthy, we keep our "work" in the open.
- **Public by Default:** All technical decisions and project management discussions happen in GitHub Issues, Pull
Requests, or the public Discussions tab.
- **Clarity in Writing:** Because we have a global community with different levels of English proficiency, please be as
explicit and clear as possible in your PR descriptions and bug reports.
### 4. Moderation & Safety
The maintainers are responsible for keeping the community a safe place to contribute.
- We reserve the right to hide comments, lock threads, or block users who repeatedly violate these principles or engage
in harassment.
- **Reporting:** If you feel you are being harassed or see behavior that is damaging the community, please reach out
privately to @daveallie.

View File

@@ -45,11 +45,13 @@ Multi-language support: Read EPUBs in various languages, including English, Span
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
## Installing ## Installing
### Web (latest firmware) ### Web (latest firmware)
1. Connect your Xteink X4 to your computer via USB-C 1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware" 2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
@@ -58,7 +60,7 @@ back to the other partition using the "Swap boot partition" button here https://
### Web (specific firmware version) ### Web (specific firmware version)
1. Connect your Xteink X4 to your computer via USB-C 1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases) 2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section 3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
@@ -82,7 +84,7 @@ See [Development](#development) below.
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository: CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
``` ```
git clone --recursive https://github.com/daveallie/crosspoint-reader git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
# Or, if you've already cloned without --recursive: # Or, if you've already cloned without --recursive:
git submodule update --init --recursive git submodule update --init --recursive
@@ -95,6 +97,25 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh ```sh
pio run --target upload pio run --target upload
``` ```
### Debugging
After flashing the new features, its recommended to capture detailed logs from the serial port.
First, make sure all required Python packages are installed:
```python
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
Minor adjustments may be required for Windows.
## Internals ## Internals
@@ -133,9 +154,12 @@ For more details on the internal file structures, see the [file formats document
Contributions are very welcome! Contributions are very welcome!
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/crosspoint-reader/discussions/categories/ideas). If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort. If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
### To submit a contribution: ### To submit a contribution:
1. Fork the repo 1. Fork the repo

50
SCOPE.md Normal file
View File

@@ -0,0 +1,50 @@
# Project Vision & Scope: CrossPoint Reader
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
## 1. Core Mission
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
usability over "swiss-army-knife" functionality.
## 2. Scope
### In-Scope
*These are features that directly improve the primary purpose of the device.*
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
engine.
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
capabilities.
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
without breaking focus.
### Out-of-Scope
*These items are rejected because they compromise the device's stability or mission.*
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
and complicate the single-core CPU's execution.
* **Media Playback:** No Audio players or Audio-books.
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips.
## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
and, critically, not distract from that reading experience.
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
> coding!

View File

@@ -13,14 +13,17 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.2 Book Selection](#32-book-selection) - [3.2 Book Selection](#32-book-selection)
- [3.3 Reading Mode](#33-reading-mode) - [3.3 Reading Mode](#33-reading-mode)
- [3.4 File Upload Screen](#34-file-upload-screen) - [3.4 File Upload Screen](#34-file-upload-screen)
- [3.4.1 Calibre Wireless Transfers](#341-calibre-wireless-transfers)
- [3.5 Settings](#35-settings) - [3.5 Settings](#35-settings)
- [3.6 Sleep Screen](#36-sleep-screen) - [3.6 Sleep Screen](#36-sleep-screen)
- [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)
- [System Navigation](#system-navigation) - [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [5. Chapter Selection Screen](#5-chapter-selection-screen) - [5. Chapter Selection Screen](#5-chapter-selection-screen)
- [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)
## 1. Hardware Overview ## 1. Hardware Overview
@@ -102,6 +105,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information - "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
- "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, fallbacks to "Custom" behavior
- **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:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary - "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected) - "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
@@ -112,7 +116,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **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
- "No Progress" - Show status bar without reading progress - "No Progress" - Show status bar without reading progress
- "Full" - Show status bar with reading progress - "Full w/ Percentage" - Show status bar with book progress (as percentage)
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
- "Book Bar Only" - Show book progress (as bar)
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown: - **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
- "Never" - Always show battery percentage (default) - "Never" - Always show battery percentage (default)
- "In Reader" - Show battery percentage everywhere except in reading mode - "In Reader" - Show battery percentage everywhere except in reading mode
@@ -148,6 +155,9 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right". - **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep. - **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting. - **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight
- "OFF" (default) - Disable the fix
- "ON" - Enable the fix
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication. - **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
- **Check for updates**: Check for firmware updates over WiFi. - **Check for updates**: Check for firmware updates over WiFi.
@@ -201,7 +211,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others. * **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others. * **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi. What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
--- ---
@@ -220,3 +230,18 @@ Accessible by pressing **Confirm** while inside a book.
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates: Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
* **Images:** Embedded images in e-books will not render. * **Images:** Embedded images in e-books will not render.
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
---
## 7. Troubleshooting Issues & Escaping Bootloop
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
```
pio device monitor
```
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).

View File

@@ -1,10 +1,18 @@
#!/bin/bash #!/usr/bin/env bash
# Check if clang-format is availible
command -v clang-format >/dev/null 2>&1 || {
printf "'clang-format' not found in current environment\n"
printf "install 'clang', 'clang-tools', or 'clang-format' depending on your distro/os and tooling requirements\n"
exit 1
}
GIT_LS_FILES_FLAGS="" GIT_LS_FILES_FLAGS=""
if [[ "$1" == "-g" ]]; then if [[ "$1" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified" GIT_LS_FILES_FLAGS="--modified"
fi fi
# --- Main Logic --- # --- Main Logic ---
# Format all files (or only modified files if -g is passed) # Format all files (or only modified files if -g is passed)
@@ -13,7 +21,9 @@ fi
# --modified: files tracked by git that have been modified (staged or unstaged) # --modified: files tracked by git that have been modified (staged or unstaged)
# --exclude-standard: ignores files in .gitignore # --exclude-standard: ignores files in .gitignore
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated. # Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
| grep -E '\.(c|cpp|h|hpp)$' \ | grep -E '\.(c|cpp|h|hpp)$' \
| grep -v -E '^lib/EpdFont/builtinFonts/' \ | grep -v -E '^lib/EpdFont/builtinFonts/' \
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
| xargs -r clang-format -style=file -i | xargs -r clang-format -style=file -i

View File

@@ -45,22 +45,9 @@ byte arrays, and emits headers under
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton `SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
in flash. in flash.
To refresh the firmware assets after updating the `.bin` files, run: A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run:
``` ```sh
./scripts/generate_hyphenation_trie.py \ ./scripts/update_hypenation.sh
--input lib/Epub/Epub/hyphenation/tries/en.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/de.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
``` ```

237
docs/i18n.md Normal file
View File

@@ -0,0 +1,237 @@
# Internationalization (I18N)
This guide explains the multi-language support system in CrossPoint Reader.
## Supported Languages
- English
- French
- German
- Portuguese
- Spanish
- Swedish
- Czech
- Russian
---
## For Developers
### Translation System Architecture
The I18N system uses **per-language YAML files** to maintain translations and a Python script to generate C++ code:
```
lib/I18n/
├── translations/ # One YAML file per language
│ ├── english.yaml
│ ├── spanish.yaml
│ ├── french.yaml
│ └── ...
├── I18n.h
├── I18n.cpp
├── I18nKeys.h # Enums (auto-generated)
├── I18nStrings.h # String array declarations (auto-generated)
└── I18nStrings.cpp # String array definitions (auto-generated)
scripts/
└── gen_i18n.py # Code generator script
```
**Key principle:** All translations are managed in the YAML files under `lib/I18n/translations/`. The Python script generates the necessary C++ code automatically.
---
### YAML File Format
Each language has its own file in `lib/I18n/translations/` (e.g. `spanish.yaml`).
A file looks like this:
```yaml
_language_name: "Español"
_language_code: "SPANISH"
_order: "1"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BOOTING"
STR_BROWSE_FILES: "Buscar archivos"
```
**Metadata keys** (prefixed with `_`):
- `_language_name` — Native display name shown to the user (e.g. "Français")
- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier.
- `_order` — Controls the position in the Language enum (English is always 0)
**Rules:**
- Use UTF-8 encoding
- Every line must follow the format: `KEY: "value"`
- Keys must be valid C++ identifiers (uppercase, strats with STR_)
- Keys must be unique within a file
- String values must be quoted
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
---
### Adding New Strings
To add a new translatable string:
#### 1. Edit the English YAML file
Add the key to `lib/I18n/translations/english.yaml`:
```yaml
STR_MY_NEW_STRING: "My New String"
```
Then add translations in each language file. If a key is missing from a
language file, the generator will automatically use the English text as a
fallback (and print a warning).
#### 2. Run the generator script
```bash
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
```
This automatically:
- Fills missing translations from English
- Updates the `StrId` enum in `I18nKeys.h`
- Regenerates all language arrays in `I18nStrings.cpp`
#### 3. Use in code
```cpp
#include <I18n.h>
// Using the tr() macro (recommended)
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
// Using I18N.get() directly
const char* text = I18N.get(StrId::STR_MY_NEW_STRING);
```
**That's it!** No manual array synchronization needed.
---
### Adding a New Language
To add support for a new language (e.g., Italian):
#### 1. Create a new YAML file
Create `lib/I18n/translations/italian.yaml`:
```yaml
_language_name: "Italiano"
_language_code: "ITALIAN"
_order: "7"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "AVVIO"
```
You only need to include the strings you have translations for. Missing
keys will fall back to English automatically.
#### 2. Run the generator
```bash
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
```
This automatically updates all necessary code.
---
### Modifying Existing Translations
Simply edit the relevant YAML file and regenerate:
```bash
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
```
---
### UTF-8 Encoding
The YAML files use UTF-8 encoding. Special characters are automatically converted to C++ UTF-8 hex sequences by the generator.
---
### I18N API Reference
```cpp
// === Convenience Macros (Recommended) ===
// tr(id) - Get translated string without StrId:: prefix
const char* text = tr(STR_SETTINGS_TITLE);
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
// I18N - Shorthand for I18n::getInstance()
I18N.setLanguage(Language::SPANISH);
Language lang = I18N.getLanguage();
// === Full API ===
// Get the singleton instance
I18n& instance = I18n::getInstance();
// Get translated string (three equivalent ways)
const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
// Set language
I18N.setLanguage(Language::SPANISH);
// Get current language
Language lang = I18N.getLanguage();
// Save language setting to file
I18N.saveSettings();
// Load language setting from file
I18N.loadSettings();
// Get character set for font subsetting (static method)
const char* chars = I18n::getCharacterSet(Language::FRENCH);
```
---
## File Storage
Language settings are stored in:
```
/.crosspoint/language.bin
```
This file contains:
- Version byte
- Current language selection (1 byte)
---
## Translation Workflow
### For Developers (Adding Features)
1. Add new strings to `lib/I18n/translations/english.yaml`
2. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
3. Use the new `StrId` in your code
4. Request translations from translators
### For Translators
1. Open the YAML file for your language in `lib/I18n/translations/`
2. Add or update translations using the format `STR_KEY: "translated text"`
3. Keep translations concise (E-ink space constraints)
4. Make sure the file is in UTF-8 encoding
5. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/` to verify
6. Test on device or submit for review

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 54 KiB

27
docs/translators.md Normal file
View File

@@ -0,0 +1,27 @@
# Translators
Below is a list of users and languages CrossPoint may support in the future.
Note because a language is below does not mean there is official support for the language at this time.
## Contributing
If you'd like to add your name to this list, please open a PR adding yourself and your Github link. Thank you!
## French
- [Spigaw](https://github.com/Spigaw)
## German
- [DavidOrtmann](https://github.com/DavidOrtmann)
## Italian
- [fragolinux](https://github.com/fragolinux)
## Russian
- [madebyKir](https://github.com/madebyKir)
## Spanish
- [yeyeto2788](https://github.com/yeyeto2788)
- [Skrzakk](https://github.com/Skrzakk)
## Swedish
- [dawiik](https://github.com/dawiik)

View File

@@ -1,14 +1,14 @@
# Web Server Guide # Web Server Guide
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone. This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload files from your computer or phone.
## Overview ## Overview
CrossPoint Reader includes a built-in web server that allows you to: CrossPoint Reader includes a built-in web server that allows you to:
- Upload EPUB files wirelessly from any device on the same WiFi network - Upload files wirelessly from any device on the same WiFi network
- Browse and manage files on your device's SD card - Browse and manage files on your device's SD card
- Create folders to organize your ebooks - Create folders to organize your library
- Delete files and folders - Delete files and folders
## Prerequisites ## Prerequisites
@@ -129,34 +129,31 @@ Click **File Manager** to access file management features.
#### Browsing Files #### Browsing Files
- The file manager displays all files and folders on your SD card - The file manager displays all files and folders on your SD card
- **Folders** are highlighted in yellow with a 📁 icon - **Folders** are highlighted in yellow and indicated with a 📁 icon
- **EPUB files** are highlighted in green with a 📗 icon - **EPUB Files** are highlighted in green and indicated with a 📗 icon
- **All Other Files** are not highlighted and indicated with a 📄 icon
- Click on a folder name to navigate into it - Click on a folder name to navigate into it
- Use the breadcrumb navigation at the top to go back to parent folders - Use the breadcrumb navigation at the top to go back to parent folders
<img src="./images/wifi/webserver_files.png" width="600"> <img src="./images/wifi/webserver_files.png" width="600">
#### Uploading EPUB Files #### Uploading Files
1. Click the **+ Add** button in the top-right corner 1. Click the **📤 Upload** button in the top-right corner
2. Select **Upload eBook** from the dropdown menu 2. Click **Choose File** and select a file from your device
3. Click **Choose File** and select an `.epub` file from your device 3. Click **Upload**
4. Click **Upload** 4. A progress bar will show the upload status
5. A progress bar will show the upload status 5. The page will automatically refresh when the upload is complete
6. The page will automatically refresh when the upload is complete
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
<img src="./images/wifi/webserver_upload.png" width="600"> <img src="./images/wifi/webserver_upload.png" width="600">
#### Creating Folders #### Creating Folders
1. Click the **+ Add** button in the top-right corner 1. Click the **📁 New Folder** button in the top-right corner
2. Select **New Folder** from the dropdown menu 2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
3. Enter a folder name (letters, numbers, underscores, and hyphens only) 3. Click **Create Folder**
4. Click **Create Folder**
This is useful for organizing your ebooks by genre, author, or series. This is useful for organizing your library by genre, author, series or file type.
#### Deleting Files and Folders #### Deleting Files and Folders
@@ -168,11 +165,25 @@ This is useful for organizing your ebooks by genre, author, or series.
**Note:** Folders must be empty before they can be deleted. **Note:** Folders must be empty before they can be deleted.
#### Moving Files
1. Click the **📂** (folder) icon next to any file
2. Enter a folder name or select one from the dropdown
3. Click **Move** to relocate the file
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
#### Renaming Files
1. Click the **✏️** (pencil) icon next to any file
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
3. Click **Rename** to permanently rename the file
--- ---
## Command Line File Management ## Command Line File Management
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md). For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. Detailed documentation can be found [here](./webserver-endpoints.md).
## Security Notes ## Security Notes
@@ -189,7 +200,6 @@ For power users, you can manage files directly from your terminal using `curl` w
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n) - **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
- **Web Server Port:** 80 (HTTP) - **Web Server Port:** 80 (HTTP)
- **Maximum Upload Size:** Limited by available SD card space - **Maximum Upload Size:** Limited by available SD card space
- **Supported File Format:** `.epub` only
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge) - **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
--- ---
@@ -198,7 +208,7 @@ For power users, you can manage files directly from your terminal using `curl` w
1. **Organize with folders** - Create folders before uploading to keep your library organized 1. **Organize with folders** - Create folders before uploading to keep your library organized
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads 2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload 3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction") 4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future 5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery 6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery

View File

@@ -1,23 +1,19 @@
#include "EpdFontFamily.h" #include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const { const EpdFont* EpdFontFamily::getFont(const Style style) const {
if (style == BOLD && bold) { // Extract font style bits (ignore UNDERLINE bit for font selection)
const bool hasBold = (style & BOLD) != 0;
const bool hasItalic = (style & ITALIC) != 0;
if (hasBold && hasItalic) {
if (boldItalic) return boldItalic;
if (bold) return bold;
if (italic) return italic;
} else if (hasBold && bold) {
return bold; return bold;
} } else if (hasItalic && italic) {
if (style == ITALIC && italic) {
return italic; return italic;
} }
if (style == BOLD_ITALIC) {
if (boldItalic) {
return boldItalic;
}
if (bold) {
return bold;
}
if (italic) {
return italic;
}
}
return regular; return regular;
} }

View File

@@ -3,7 +3,7 @@
class EpdFontFamily { class EpdFontFamily {
public: public:
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 }; enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
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)

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#ifndef OMIT_BOOKERLY
#include <builtinFonts/bookerly_12_bold.h> #include <builtinFonts/bookerly_12_bold.h>
#include <builtinFonts/bookerly_12_bolditalic.h> #include <builtinFonts/bookerly_12_bolditalic.h>
#include <builtinFonts/bookerly_12_italic.h> #include <builtinFonts/bookerly_12_italic.h>
@@ -16,7 +17,10 @@
#include <builtinFonts/bookerly_18_bolditalic.h> #include <builtinFonts/bookerly_18_bolditalic.h>
#include <builtinFonts/bookerly_18_italic.h> #include <builtinFonts/bookerly_18_italic.h>
#include <builtinFonts/bookerly_18_regular.h> #include <builtinFonts/bookerly_18_regular.h>
#endif // OMIT_BOOKERLY
#include <builtinFonts/notosans_8_regular.h> #include <builtinFonts/notosans_8_regular.h>
#ifndef OMIT_NOTOSANS
#include <builtinFonts/notosans_12_bold.h> #include <builtinFonts/notosans_12_bold.h>
#include <builtinFonts/notosans_12_bolditalic.h> #include <builtinFonts/notosans_12_bolditalic.h>
#include <builtinFonts/notosans_12_italic.h> #include <builtinFonts/notosans_12_italic.h>
@@ -33,6 +37,9 @@
#include <builtinFonts/notosans_18_bolditalic.h> #include <builtinFonts/notosans_18_bolditalic.h>
#include <builtinFonts/notosans_18_italic.h> #include <builtinFonts/notosans_18_italic.h>
#include <builtinFonts/notosans_18_regular.h> #include <builtinFonts/notosans_18_regular.h>
#endif // OMIT_NOTOSANS
#ifndef OMIT_OPENDYSLEXIC
#include <builtinFonts/opendyslexic_10_bold.h> #include <builtinFonts/opendyslexic_10_bold.h>
#include <builtinFonts/opendyslexic_10_bolditalic.h> #include <builtinFonts/opendyslexic_10_bolditalic.h>
#include <builtinFonts/opendyslexic_10_italic.h> #include <builtinFonts/opendyslexic_10_italic.h>
@@ -49,6 +56,8 @@
#include <builtinFonts/opendyslexic_8_bolditalic.h> #include <builtinFonts/opendyslexic_8_bolditalic.h>
#include <builtinFonts/opendyslexic_8_italic.h> #include <builtinFonts/opendyslexic_8_italic.h>
#include <builtinFonts/opendyslexic_8_regular.h> #include <builtinFonts/opendyslexic_8_regular.h>
#endif // OMIT_OPENDYSLEXIC
#include <builtinFonts/ubuntu_10_bold.h> #include <builtinFonts/ubuntu_10_bold.h>
#include <builtinFonts/ubuntu_10_regular.h> #include <builtinFonts/ubuntu_10_regular.h>
#include <builtinFonts/ubuntu_12_bold.h> #include <builtinFonts/ubuntu_12_bold.h>

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
#include <vector> #include <vector>
#include "Epub/BookMetadataCache.h" #include "Epub/BookMetadataCache.h"
#include "Epub/css/CssParser.h"
class ZipFile; class ZipFile;
@@ -24,11 +25,16 @@ class Epub {
std::string cachePath; std::string cachePath;
// Spine and TOC cache // Spine and TOC cache
std::unique_ptr<BookMetadataCache> bookMetadataCache; std::unique_ptr<BookMetadataCache> bookMetadataCache;
// CSS parser for styling
std::unique_ptr<CssParser> cssParser;
// CSS files
std::vector<std::string> cssFiles;
bool findContentOpfFile(std::string* contentOpfFile) const; bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata); bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
bool parseTocNcxFile() const; bool parseTocNcxFile() const;
bool parseTocNavFile() const; bool parseTocNavFile() const;
void parseCssFiles() const;
public: public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@@ -37,7 +43,7 @@ class Epub {
} }
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
bool load(bool buildIfMissing = true); bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
const std::string& getCachePath() const; const std::string& getCachePath() const;
@@ -46,9 +52,23 @@ class Epub {
const std::string& getAuthor() const; const std::string& getAuthor() const;
const std::string& getLanguage() const; const std::string& getLanguage() const;
std::string getCoverBmpPath(bool cropped = false) const; std::string getCoverBmpPath(bool cropped = false) const;
// Generate a 1-bit BMP cover image from the EPUB cover image.
// Returns true on success. On conversion failure, callers may use
// `generateInvalidFormatCoverBmp` to create a valid marker BMP.
bool generateCoverBmp(bool cropped = false) const; bool generateCoverBmp(bool cropped = false) const;
// Create a valid 1-bit BMP that visually indicates an invalid/unsupported
// cover format (an X pattern). This prevents repeated generation attempts
// by providing a valid BMP file that `isValidThumbnailBmp` accepts.
bool generateInvalidFormatCoverBmp(bool cropped = false) const;
std::string getThumbBmpPath() const; std::string getThumbBmpPath() const;
bool generateThumbBmp() const; std::string getThumbBmpPath(int height) const;
// Generate a thumbnail BMP at the requested `height`. Returns true on
// successful conversion. If conversion fails, `generateInvalidFormatThumbBmp`
// can be used to write a valid marker image that prevents retries.
bool generateThumbBmp(int height) const;
// Create a valid 1-bit thumbnail BMP with an X marker indicating an
// invalid/unsupported cover image instead of leaving an empty marker file.
bool generateInvalidFormatThumbBmp(int height) const;
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr, uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const; bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
@@ -64,4 +84,10 @@ class Epub {
size_t getBookSize() const; size_t getBookSize() const;
float calculateProgress(int currentSpineIndex, float currentSpineRead) const; float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
CssParser* getCssParser() const { return cssParser.get(); }
static bool isValidThumbnailBmp(const std::string& bmpPath);
private:
std::vector<std::string> getCoverCandidates() const;
}; };

View File

@@ -1,6 +1,6 @@
#include "BookMetadataCache.h" #include "BookMetadataCache.h"
#include <HardwareSerial.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <ZipFile.h> #include <ZipFile.h>
@@ -21,15 +21,15 @@ bool BookMetadataCache::beginWrite() {
buildMode = true; buildMode = true;
spineCount = 0; spineCount = 0;
tocCount = 0; tocCount = 0;
Serial.printf("[%lu] [BMC] Entering write mode\n", millis()); LOG_DBG("BMC", "Entering write mode");
return true; return true;
} }
bool BookMetadataCache::beginContentOpfPass() { bool BookMetadataCache::beginContentOpfPass() {
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis()); LOG_DBG("BMC", "Beginning content opf pass");
// Open spine file for writing // Open spine file for writing
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
} }
bool BookMetadataCache::endContentOpfPass() { bool BookMetadataCache::endContentOpfPass() {
@@ -38,12 +38,12 @@ bool BookMetadataCache::endContentOpfPass() {
} }
bool BookMetadataCache::beginTocPass() { bool BookMetadataCache::beginTocPass() {
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis()); LOG_DBG("BMC", "Beginning toc pass");
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
return false; return false;
} }
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
spineFile.close(); spineFile.close();
return false; return false;
} }
@@ -66,7 +66,7 @@ bool BookMetadataCache::beginTocPass() {
}); });
spineFile.seek(0); spineFile.seek(0);
useSpineHrefIndex = true; useSpineHrefIndex = true;
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount); LOG_DBG("BMC", "Using fast index for %d spine items", spineCount);
} else { } else {
useSpineHrefIndex = false; useSpineHrefIndex = false;
} }
@@ -87,27 +87,27 @@ bool BookMetadataCache::endTocPass() {
bool BookMetadataCache::endWrite() { bool BookMetadataCache::endWrite() {
if (!buildMode) { if (!buildMode) {
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis()); LOG_DBG("BMC", "endWrite called but not in build mode");
return false; return false;
} }
buildMode = false; buildMode = false;
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount); LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
return true; return true;
} }
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) { bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
// Open all three files, writing to meta, reading from spine and toc // Open all three files, writing to meta, reading from spine and toc
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
return false; return false;
} }
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
bookFile.close(); bookFile.close();
return false; return false;
} }
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
bookFile.close(); bookFile.close();
spineFile.close(); spineFile.close();
return false; return false;
@@ -167,7 +167,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
ZipFile zip(epubPath); ZipFile zip(epubPath);
// Pre-open zip file to speed up size calculations // Pre-open zip file to speed up size calculations
if (!zip.open()) { if (!zip.open()) {
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis()); LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
bookFile.close(); bookFile.close();
spineFile.close(); spineFile.close();
tocFile.close(); tocFile.close();
@@ -185,7 +185,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
bool useBatchSizes = false; bool useBatchSizes = false;
if (spineCount >= LARGE_SPINE_THRESHOLD) { if (spineCount >= LARGE_SPINE_THRESHOLD) {
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount); LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
std::vector<ZipFile::SizeTarget> targets; std::vector<ZipFile::SizeTarget> targets;
targets.reserve(spineCount); targets.reserve(spineCount);
@@ -208,7 +208,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
spineSizes.resize(spineCount, 0); spineSizes.resize(spineCount, 0);
int matched = zip.fillUncompressedSizes(targets, spineSizes); int matched = zip.fillUncompressedSizes(targets, spineSizes);
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount); LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", matched, spineCount);
targets.clear(); targets.clear();
targets.shrink_to_fit(); targets.shrink_to_fit();
@@ -227,9 +227,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs // Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
// Logging here is for debugging // Logging here is for debugging
if (spineEntry.tocIndex == -1) { if (spineEntry.tocIndex == -1) {
Serial.printf( LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
"[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s, using title from last section\n", spineEntry.href.c_str());
millis(), i, spineEntry.href.c_str());
spineEntry.tocIndex = lastSpineTocIndex; spineEntry.tocIndex = lastSpineTocIndex;
} }
lastSpineTocIndex = spineEntry.tocIndex; lastSpineTocIndex = spineEntry.tocIndex;
@@ -240,13 +239,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
if (itemSize == 0) { if (itemSize == 0) {
const std::string path = FsHelpers::normalisePath(spineEntry.href); const std::string path = FsHelpers::normalisePath(spineEntry.href);
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) { if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str()); LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
} }
} }
} else { } else {
const std::string path = FsHelpers::normalisePath(spineEntry.href); const std::string path = FsHelpers::normalisePath(spineEntry.href);
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) { if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str()); LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
} }
} }
@@ -270,16 +269,16 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
spineFile.close(); spineFile.close();
tocFile.close(); tocFile.close();
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis()); LOG_DBG("BMC", "Successfully built book.bin");
return true; return true;
} }
bool BookMetadataCache::cleanupTmpFiles() const { bool BookMetadataCache::cleanupTmpFiles() const {
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) { if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
SdMan.remove((cachePath + tmpSpineBinFile).c_str()); Storage.remove((cachePath + tmpSpineBinFile).c_str());
} }
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) { if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
SdMan.remove((cachePath + tmpTocBinFile).c_str()); Storage.remove((cachePath + tmpTocBinFile).c_str());
} }
return true; return true;
} }
@@ -306,7 +305,7 @@ uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) c
// this is because in this function we're marking positions of the items // this is because in this function we're marking positions of the items
void BookMetadataCache::createSpineEntry(const std::string& href) { void BookMetadataCache::createSpineEntry(const std::string& href) {
if (!buildMode || !spineFile) { if (!buildMode || !spineFile) {
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis()); LOG_DBG("BMC", "createSpineEntry called but not in build mode");
return; return;
} }
@@ -318,7 +317,7 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
const uint8_t level) { const uint8_t level) {
if (!buildMode || !tocFile || !spineFile) { if (!buildMode || !tocFile || !spineFile) {
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis()); LOG_DBG("BMC", "createTocEntry called but not in build mode");
return; return;
} }
@@ -340,7 +339,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
} }
if (spineIndex == -1) { if (spineIndex == -1) {
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str()); LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
} }
} else { } else {
spineFile.seek(0); spineFile.seek(0);
@@ -352,7 +351,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
} }
} }
if (spineIndex == -1) { if (spineIndex == -1) {
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str()); LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
} }
} }
@@ -364,14 +363,14 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
/* ============= READING / LOADING FUNCTIONS ================ */ /* ============= READING / LOADING FUNCTIONS ================ */
bool BookMetadataCache::load() { bool BookMetadataCache::load() {
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
return false; return false;
} }
uint8_t version; uint8_t version;
serialization::readPod(bookFile, version); serialization::readPod(bookFile, version);
if (version != BOOK_CACHE_VERSION) { if (version != BOOK_CACHE_VERSION) {
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version); LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
bookFile.close(); bookFile.close();
return false; return false;
} }
@@ -387,18 +386,18 @@ bool BookMetadataCache::load() {
serialization::readString(bookFile, coreMetadata.textReferenceHref); serialization::readString(bookFile, coreMetadata.textReferenceHref);
loaded = true; loaded = true;
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount); LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
return true; return true;
} }
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) { BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
if (!loaded) { if (!loaded) {
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis()); LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
return {}; return {};
} }
if (index < 0 || index >= static_cast<int>(spineCount)) { if (index < 0 || index >= static_cast<int>(spineCount)) {
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index); LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
return {}; return {};
} }
@@ -412,12 +411,12 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) { BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
if (!loaded) { if (!loaded) {
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis()); LOG_ERR("BMC", "getTocEntry called but cache not loaded");
return {}; return {};
} }
if (index < 0 || index >= static_cast<int>(tocCount)) { if (index < 0 || index >= static_cast<int>(tocCount)) {
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index); LOG_ERR("BMC", "getTocEntry index %d out of range", index);
return {}; return {};
} }

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <SDCardManager.h> #include <HalStorage.h>
#include <algorithm> #include <algorithm>
#include <string> #include <string>

View File

@@ -1,8 +1,17 @@
#include "Page.h" #include "Page.h"
#include <HardwareSerial.h> #include <GfxRenderer.h>
#include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
// Cell padding in pixels (must match TABLE_CELL_PAD_* in ChapterHtmlSlimParser.cpp)
static constexpr int TABLE_CELL_PADDING_X = 4;
static constexpr int TABLE_CELL_PADDING_TOP = 1;
// ---------------------------------------------------------------------------
// PageLine
// ---------------------------------------------------------------------------
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);
} }
@@ -25,6 +34,142 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos)); return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
} }
// ---------------------------------------------------------------------------
// PageImage
// ---------------------------------------------------------------------------
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
// Images don't use fontId or text rendering
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
}
bool PageImage::serialize(FsFile& file) {
serialization::writePod(file, xPos);
serialization::writePod(file, yPos);
// serialize ImageBlock
return imageBlock->serialize(file);
}
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
int16_t xPos;
int16_t yPos;
serialization::readPod(file, xPos);
serialization::readPod(file, yPos);
auto ib = ImageBlock::deserialize(file);
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
}
// ---------------------------------------------------------------------------
// PageTableRow
// ---------------------------------------------------------------------------
void PageTableRow::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
const int baseX = xPos + xOffset;
const int baseY = yPos + yOffset;
// Draw horizontal borders (top and bottom of this row)
renderer.drawLine(baseX, baseY, baseX + totalWidth, baseY);
renderer.drawLine(baseX, baseY + rowHeight, baseX + totalWidth, baseY + rowHeight);
// Draw vertical borders and render cell contents
// Left edge
renderer.drawLine(baseX, baseY, baseX, baseY + rowHeight);
for (const auto& cell : cells) {
// Right vertical border for this cell
const int cellRightX = baseX + cell.xOffset + cell.columnWidth;
renderer.drawLine(cellRightX, baseY, cellRightX, baseY + rowHeight);
// Render each text line within the cell
const int cellTextX = baseX + cell.xOffset + TABLE_CELL_PADDING_X;
int cellLineY = baseY + 1 + TABLE_CELL_PADDING_TOP; // 1px border + top padding
for (const auto& line : cell.lines) {
line->render(renderer, fontId, cellTextX, cellLineY);
cellLineY += lineHeight;
}
}
}
bool PageTableRow::serialize(FsFile& file) {
serialization::writePod(file, xPos);
serialization::writePod(file, yPos);
serialization::writePod(file, rowHeight);
serialization::writePod(file, totalWidth);
serialization::writePod(file, lineHeight);
const uint16_t cellCount = static_cast<uint16_t>(cells.size());
serialization::writePod(file, cellCount);
for (const auto& cell : cells) {
serialization::writePod(file, cell.xOffset);
serialization::writePod(file, cell.columnWidth);
const uint16_t lineCount = static_cast<uint16_t>(cell.lines.size());
serialization::writePod(file, lineCount);
for (const auto& line : cell.lines) {
if (!line->serialize(file)) {
return false;
}
}
}
return true;
}
std::unique_ptr<PageTableRow> PageTableRow::deserialize(FsFile& file) {
int16_t xPos, yPos, rowHeight, totalWidth, lineHeight;
serialization::readPod(file, xPos);
serialization::readPod(file, yPos);
serialization::readPod(file, rowHeight);
serialization::readPod(file, totalWidth);
serialization::readPod(file, lineHeight);
uint16_t cellCount;
serialization::readPod(file, cellCount);
// Sanity check
if (cellCount > 100) {
LOG_ERR("PTR", "Deserialization failed: cell count %u exceeds maximum", cellCount);
return nullptr;
}
std::vector<PageTableCellData> cells;
cells.resize(cellCount);
for (uint16_t c = 0; c < cellCount; ++c) {
serialization::readPod(file, cells[c].xOffset);
serialization::readPod(file, cells[c].columnWidth);
uint16_t lineCount;
serialization::readPod(file, lineCount);
if (lineCount > 1000) {
LOG_ERR("PTR", "Deserialization failed: line count %u in cell %u exceeds maximum", lineCount, c);
return nullptr;
}
cells[c].lines.reserve(lineCount);
for (uint16_t l = 0; l < lineCount; ++l) {
auto tb = TextBlock::deserialize(file);
if (!tb) {
return nullptr;
}
cells[c].lines.push_back(std::move(tb));
}
}
return std::unique_ptr<PageTableRow>(
new PageTableRow(std::move(cells), rowHeight, totalWidth, lineHeight, xPos, yPos));
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
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) { for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset); element->render(renderer, fontId, xOffset, yOffset);
@@ -36,8 +181,7 @@ bool Page::serialize(FsFile& file) const {
serialization::writePod(file, count); serialization::writePod(file, count);
for (const auto& el : elements) { for (const auto& el : elements) {
// Only PageLine exists currently serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
if (!el->serialize(file)) { if (!el->serialize(file)) {
return false; return false;
} }
@@ -59,11 +203,68 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
if (tag == TAG_PageLine) { if (tag == TAG_PageLine) {
auto pl = PageLine::deserialize(file); auto pl = PageLine::deserialize(file);
page->elements.push_back(std::move(pl)); page->elements.push_back(std::move(pl));
} else if (tag == TAG_PageTableRow) {
auto tr = PageTableRow::deserialize(file);
if (!tr) {
LOG_ERR("PGE", "Deserialization failed for PageTableRow at element %u", i);
return nullptr;
}
page->elements.push_back(std::move(tr));
} else if (tag == TAG_PageImage) {
auto pi = PageImage::deserialize(file);
page->elements.push_back(std::move(pi));
} else { } else {
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag); LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
return nullptr; return nullptr;
} }
} }
return page; return page;
} }
bool Page::getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const {
bool firstImage = true;
for (const auto& el : elements) {
if (el->getTag() == TAG_PageImage) {
PageImage* pi = static_cast<PageImage*>(el.get());
ImageBlock* ib = pi->getImageBlock();
if (firstImage) {
// Initialize with first image bounds
outX = pi->xPos;
outY = pi->yPos;
outWidth = ib->getWidth();
outHeight = ib->getHeight();
firstImage = false;
} else {
// Expand bounding box to include this image
int imgX = pi->xPos;
int imgY = pi->yPos;
int imgW = ib->getWidth();
int imgH = ib->getHeight();
// Expand right boundary
if (imgX + imgW > outX + outWidth) {
outWidth = (imgX + imgW) - outX;
}
// Expand left boundary
if (imgX < outX) {
int oldRight = outX + outWidth;
outX = imgX;
outWidth = oldRight - outX;
}
// Expand bottom boundary
if (imgY + imgH > outY + outHeight) {
outHeight = (imgY + imgH) - outY;
}
// Expand top boundary
if (imgY < outY) {
int oldBottom = outY + outHeight;
outY = imgY;
outHeight = oldBottom - outY;
}
}
}
}
return !firstImage; // Return true if at least one image was found
}

View File

@@ -1,13 +1,17 @@
#pragma once #pragma once
#include <SdFat.h> #include <HalStorage.h>
#include <algorithm>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "blocks/ImageBlock.h"
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
enum PageElementTag : uint8_t { enum PageElementTag : uint8_t {
TAG_PageLine = 1, TAG_PageLine = 1,
TAG_PageTableRow = 2,
TAG_PageImage = 3,
}; };
// represents something that has been added to a page // represents something that has been added to a page
@@ -17,6 +21,7 @@ class PageElement {
int16_t yPos; int16_t yPos;
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default; virtual ~PageElement() = default;
virtual PageElementTag getTag() const = 0;
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0; virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
virtual bool serialize(FsFile& file) = 0; virtual bool serialize(FsFile& file) = 0;
}; };
@@ -28,11 +33,59 @@ class PageLine final : public PageElement {
public: public:
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos) PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), block(std::move(block)) {} : PageElement(xPos, yPos), block(std::move(block)) {}
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
PageElementTag getTag() const override { return TAG_PageLine; }
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override; bool serialize(FsFile& file) override;
static std::unique_ptr<PageLine> deserialize(FsFile& file); static std::unique_ptr<PageLine> deserialize(FsFile& file);
}; };
/// Data for a single cell within a PageTableRow.
struct PageTableCellData {
std::vector<std::shared_ptr<TextBlock>> lines; // Laid-out text lines for this cell
uint16_t columnWidth = 0; // Width of this column in pixels
uint16_t xOffset = 0; // X offset of this cell within the row
};
/// A table row element that renders cells in a column-aligned grid with borders.
class PageTableRow final : public PageElement {
std::vector<PageTableCellData> cells;
int16_t rowHeight; // Total row height in pixels
int16_t totalWidth; // Total table width in pixels
int16_t lineHeight; // Height of one text line (for vertical positioning of cell lines)
public:
PageTableRow(std::vector<PageTableCellData> cells, int16_t rowHeight, int16_t totalWidth, int16_t lineHeight,
int16_t xPos, int16_t yPos)
: PageElement(xPos, yPos),
cells(std::move(cells)),
rowHeight(rowHeight),
totalWidth(totalWidth),
lineHeight(lineHeight) {}
int16_t getHeight() const { return rowHeight; }
PageElementTag getTag() const override { return TAG_PageTableRow; }
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
static std::unique_ptr<PageTableRow> deserialize(FsFile& file);
};
// An image element on a page
class PageImage final : public PageElement {
std::shared_ptr<ImageBlock> imageBlock;
public:
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
PageElementTag getTag() const override { return TAG_PageImage; }
static std::unique_ptr<PageImage> deserialize(FsFile& file);
// Helper to get image block dimensions (needed for bounding box calculation)
ImageBlock* getImageBlock() const { return imageBlock.get(); }
};
class Page { class Page {
public: public:
// the list of block index and line numbers on this page // the list of block index and line numbers on this page
@@ -40,4 +93,15 @@ class Page {
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(FsFile& file) const; bool serialize(FsFile& file) const;
static std::unique_ptr<Page> deserialize(FsFile& file); static std::unique_ptr<Page> deserialize(FsFile& file);
// Check if page contains any images (used to force full refresh)
bool hasImages() const {
return std::any_of(elements.begin(), elements.end(),
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
}
// Get the bounding box of all images on this page.
// Returns true if page has images and fills out the bounding box coordinates.
// If no images, returns false.
bool getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const;
}; };

View File

@@ -5,7 +5,6 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <functional> #include <functional>
#include <iterator>
#include <limits> #include <limits>
#include <vector> #include <vector>
@@ -32,6 +31,9 @@ void stripSoftHyphensInPlace(std::string& word) {
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen. // Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word, uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
const EpdFontFamily::Style style, const bool appendHyphen = false) { const EpdFontFamily::Style style, const bool appendHyphen = false) {
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
return renderer.getSpaceWidth(fontId);
}
const bool hasSoftHyphen = containsSoftHyphen(word); const bool hasSoftHyphen = containsSoftHyphen(word);
if (!hasSoftHyphen && !appendHyphen) { if (!hasSoftHyphen && !appendHyphen) {
return renderer.getTextWidth(fontId, word.c_str(), style); return renderer.getTextWidth(fontId, word.c_str(), style);
@@ -49,11 +51,24 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
} // namespace } // namespace
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) { void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
const bool attachToPrevious) {
if (word.empty()) return; if (word.empty()) return;
words.push_back(std::move(word)); words.push_back(std::move(word));
wordStyles.push_back(fontStyle); EpdFontFamily::Style combinedStyle = fontStyle;
if (underline) {
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
}
wordStyles.push_back(combinedStyle);
wordContinues.push_back(attachToPrevious);
forceBreakAfter.push_back(false);
}
void ParsedText::addLineBreak() {
if (!words.empty()) {
forceBreakAfter.back() = true;
}
} }
// Consumes data to minimize memory usage // Consumes data to minimize memory usage
@@ -70,49 +85,52 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
const int pageWidth = viewportWidth; const int pageWidth = viewportWidth;
const int spaceWidth = renderer.getSpaceWidth(fontId); const int spaceWidth = renderer.getSpaceWidth(fontId);
auto wordWidths = calculateWordWidths(renderer, fontId); auto wordWidths = calculateWordWidths(renderer, fontId);
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, spaceWidth, wordWidths); lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
} else { } else {
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths); lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
} }
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, spaceWidth, wordWidths, lineBreakIndices, processLine); extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine);
} }
} }
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) { std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
const size_t totalWordCount = words.size();
std::vector<uint16_t> wordWidths; std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount); wordWidths.reserve(words.size());
auto wordsIt = words.begin(); for (size_t i = 0; i < words.size(); ++i) {
auto wordStylesIt = wordStyles.begin(); wordWidths.push_back(measureWordWidth(renderer, fontId, words[i], wordStyles[i]));
while (wordsIt != words.end()) {
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
std::advance(wordsIt, 1);
std::advance(wordStylesIt, 1);
} }
return wordWidths; return wordWidths;
} }
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,
const int spaceWidth, std::vector<uint16_t>& wordWidths) { const int spaceWidth, std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) {
if (words.empty()) { if (words.empty()) {
return {}; return {};
} }
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
const int firstLineIndent =
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation. // Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
for (size_t i = 0; i < wordWidths.size(); ++i) { for (size_t i = 0; i < wordWidths.size(); ++i) {
while (wordWidths[i] > pageWidth) { // First word needs to fit in reduced width if there's an indent
if (!hyphenateWordAtIndex(i, pageWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) { const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
while (wordWidths[i] > effectiveWidth) {
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
break; break;
} }
} }
@@ -130,22 +148,39 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
ans[totalWordCount - 1] = totalWordCount - 1; ans[totalWordCount - 1] = totalWordCount - 1;
for (int i = totalWordCount - 2; i >= 0; --i) { for (int i = totalWordCount - 2; i >= 0; --i) {
int currlen = -spaceWidth; int currlen = 0;
dp[i] = MAX_COST; dp[i] = MAX_COST;
for (size_t j = i; j < totalWordCount; ++j) { // First line has reduced width due to text-indent
// Current line length: previous width + space + current word width const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
currlen += wordWidths[j] + spaceWidth;
if (currlen > pageWidth) { for (size_t j = i; j < totalWordCount; ++j) {
// If the previous word has a forced line break, this line cannot include word j
if (j > static_cast<size_t>(i) && !forceBreakAfter.empty() && forceBreakAfter[j - 1]) {
break; break;
} }
// Add space before word j, unless it's the first word on the line or a continuation
const int gap = j > static_cast<size_t>(i) && !continuesVec[j] ? spaceWidth : 0;
currlen += wordWidths[j] + gap;
if (currlen > effectivePageWidth) {
break;
}
// Forced line break after word j overrides continuation (must end line here)
const bool mustBreakHere = !forceBreakAfter.empty() && forceBreakAfter[j];
// Cannot break after word j if the next word attaches to it (unless forced)
if (!mustBreakHere && j + 1 < totalWordCount && continuesVec[j + 1]) {
continue;
}
int cost; int cost;
if (j == totalWordCount - 1) { if (j == totalWordCount - 1) {
cost = 0; // Last line cost = 0; // Last line
} else { } else {
const int remainingSpace = pageWidth - currlen; const int remainingSpace = effectivePageWidth - currlen;
// Use long long for the square to prevent overflow // Use long long for the square to prevent overflow
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1]; const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
@@ -160,6 +195,11 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
dp[i] = cost; dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line ans[i] = j; // j is the index of the last word in this optimal line
} }
// After evaluating cost, enforce forced break - no more words on this line
if (mustBreakHere) {
break;
}
} }
// Handle oversized word: if no valid configuration found, force single-word line // Handle oversized word: if no valid configuration found, force single-word line
@@ -200,7 +240,11 @@ void ParsedText::applyParagraphIndent() {
return; return;
} }
if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) { if (blockStyle.textIndentDefined) {
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
// The actual indent positioning is handled in extractLine()
} else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) {
// No CSS text-indent defined - use EmSpace fallback for visual indent
words.front().insert(0, "\xe2\x80\x83"); words.front().insert(0, "\xe2\x80\x83");
} }
} }
@@ -208,29 +252,51 @@ void ParsedText::applyParagraphIndent() {
// 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, const int spaceWidth, const int pageWidth, const int spaceWidth,
std::vector<uint16_t>& wordWidths) { std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) {
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
const int firstLineIndent =
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
std::vector<size_t> lineBreakIndices; std::vector<size_t> lineBreakIndices;
size_t currentIndex = 0; size_t currentIndex = 0;
bool isFirstLine = true;
while (currentIndex < wordWidths.size()) { while (currentIndex < wordWidths.size()) {
const size_t lineStart = currentIndex; const size_t lineStart = currentIndex;
int lineWidth = 0; int lineWidth = 0;
// First line has reduced width due to text-indent
const int effectivePageWidth = isFirstLine ? pageWidth - firstLineIndent : pageWidth;
// Consume as many words as possible for current line, splitting when prefixes fit // Consume as many words as possible for current line, splitting when prefixes fit
while (currentIndex < wordWidths.size()) { while (currentIndex < wordWidths.size()) {
// If the previous word has a forced line break, stop - this word starts a new line
if (currentIndex > lineStart && !forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
break;
}
const bool isFirstWord = currentIndex == lineStart; const bool isFirstWord = currentIndex == lineStart;
const int spacing = isFirstWord ? 0 : spaceWidth; const int spacing = isFirstWord || continuesVec[currentIndex] ? 0 : spaceWidth;
const int candidateWidth = spacing + wordWidths[currentIndex]; const int candidateWidth = spacing + wordWidths[currentIndex];
// Word fits on current line // Word fits on current line
if (lineWidth + candidateWidth <= pageWidth) { if (lineWidth + candidateWidth <= effectivePageWidth) {
lineWidth += candidateWidth; lineWidth += candidateWidth;
++currentIndex; ++currentIndex;
// If the word we just added has a forced break, end this line now
if (!forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
break;
}
continue; continue;
} }
// Word would overflow — try to split based on hyphenation points // Word would overflow — try to split based on hyphenation points
const int availableWidth = pageWidth - lineWidth - spacing; const int availableWidth = effectivePageWidth - lineWidth - spacing;
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
if (availableWidth > 0 && if (availableWidth > 0 &&
@@ -249,7 +315,19 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
break; break;
} }
// Don't break before a continuation word (e.g., orphaned "?" after "question").
// Backtrack to the start of the continuation group so the whole group moves to the next line.
// But don't backtrack past a forced break point.
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
// Don't backtrack past a forced break
if (!forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
break;
}
--currentIndex;
}
lineBreakIndices.push_back(currentIndex); lineBreakIndices.push_back(currentIndex);
isFirstLine = false;
} }
return lineBreakIndices; return lineBreakIndices;
@@ -265,14 +343,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
return false; return false;
} }
// Get iterators to target word and style. const std::string& word = words[wordIndex];
auto wordIt = words.begin(); const auto style = wordStyles[wordIndex];
auto styleIt = wordStyles.begin();
std::advance(wordIt, wordIndex);
std::advance(styleIt, wordIndex);
const std::string& word = *wordIt;
const auto style = *styleIt;
// Collect candidate breakpoints (byte offsets and hyphen requirements). // Collect candidate breakpoints (byte offsets and hyphen requirements).
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks); auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
@@ -309,16 +381,27 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
// Split the word at the selected breakpoint and append a hyphen if required. // Split the word at the selected breakpoint and append a hyphen if required.
std::string remainder = word.substr(chosenOffset); std::string remainder = word.substr(chosenOffset);
wordIt->resize(chosenOffset); words[wordIndex].resize(chosenOffset);
if (chosenNeedsHyphen) { if (chosenNeedsHyphen) {
wordIt->push_back('-'); words[wordIndex].push_back('-');
} }
// Insert the remainder word (with matching style) directly after the prefix. // Insert the remainder word (with matching style and continuation flag) directly after the prefix.
auto insertWordIt = std::next(wordIt); words.insert(words.begin() + wordIndex + 1, remainder);
auto insertStyleIt = std::next(styleIt); wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
words.insert(insertWordIt, remainder);
wordStyles.insert(insertStyleIt, style); // The remainder inherits whatever continuation status the original word had with the word after it.
const bool originalContinuedToNext = wordContinues[wordIndex];
// The original word (now prefix) does NOT continue to remainder (hyphen separates them)
wordContinues[wordIndex] = false;
wordContinues.insert(wordContinues.begin() + wordIndex + 1, originalContinuedToNext);
// Forced break belongs to the original whole word; transfer it to the remainder (last part).
if (!forceBreakAfter.empty()) {
const bool originalForceBreak = forceBreakAfter[wordIndex];
forceBreakAfter[wordIndex] = false; // prefix doesn't force break
forceBreakAfter.insert(forceBreakAfter.begin() + wordIndex + 1, originalForceBreak);
}
// 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);
@@ -328,55 +411,74 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
} }
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices, const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) { const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
const size_t lineBreak = lineBreakIndices[breakIndex]; const size_t lineBreak = lineBreakIndices[breakIndex];
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
const size_t lineWordCount = lineBreak - lastBreakAt; const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate total word width for this line // Calculate first line indent (only for left/justified text without extra paragraph spacing)
const bool isFirstLine = breakIndex == 0;
const int firstLineIndent =
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
? blockStyle.textIndent
: 0;
// Calculate total word width for this line and count actual word gaps
// (continuation words attach to previous word with no gap)
int lineWordWidthSum = 0; int lineWordWidthSum = 0;
for (size_t i = lastBreakAt; i < lineBreak; i++) { size_t actualGapCount = 0;
lineWordWidthSum += wordWidths[i];
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
// Count gaps: each word after the first creates a gap, unless it's a continuation
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
actualGapCount++;
}
} }
// Calculate spacing // Calculate spacing (account for indent reducing effective page width on first line)
const int spareSpace = pageWidth - lineWordWidthSum; const int effectivePageWidth = pageWidth - firstLineIndent;
const int spareSpace = effectivePageWidth - lineWordWidthSum;
int spacing = spaceWidth; int spacing = spaceWidth;
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1; const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) { // For justified text, calculate spacing based on actual gap count
spacing = spareSpace / (lineWordCount - 1); if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1) {
spacing = spareSpace / static_cast<int>(actualGapCount);
} }
// Calculate initial x position // Calculate initial x position (first line starts at indent for left/justified text)
uint16_t xpos = 0; auto xpos = static_cast<uint16_t>(firstLineIndent);
if (style == TextBlock::RIGHT_ALIGN) { if (blockStyle.alignment == CssTextAlign::Right) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth; xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
} else if (style == TextBlock::CENTER_ALIGN) { } else if (blockStyle.alignment == CssTextAlign::Center) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2; xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 2;
} }
// Pre-calculate X positions for words // Pre-calculate X positions for words
std::list<uint16_t> lineXPos; // Continuation words attach to the previous word with no space before them
for (size_t i = lastBreakAt; i < lineBreak; i++) { std::vector<uint16_t> lineXPos;
const uint16_t currentWordWidth = wordWidths[i]; lineXPos.reserve(lineWordCount);
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
lineXPos.push_back(xpos); lineXPos.push_back(xpos);
xpos += currentWordWidth + spacing;
// Add spacing after this word, unless the next word is a continuation
const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1];
xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing);
} }
// Iterators always start at the beginning as we are moving content with splice below // Build line data by moving from the original vectors using index range
auto wordEndIt = words.begin(); std::vector<std::string> lineWords(std::make_move_iterator(words.begin() + lastBreakAt),
auto wordStyleEndIt = wordStyles.begin(); std::make_move_iterator(words.begin() + lineBreak));
std::advance(wordEndIt, lineWordCount); std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
std::advance(wordStyleEndIt, lineWordCount);
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
std::list<std::string> lineWords;
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
std::list<EpdFontFamily::Style> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
for (auto& word : lineWords) { for (auto& word : lineWords) {
if (containsSoftHyphen(word)) { if (containsSoftHyphen(word)) {
@@ -384,5 +486,25 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
} }
} }
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); processLine(
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
}
uint16_t ParsedText::getNaturalWidth(const GfxRenderer& renderer, const int fontId) const {
if (words.empty()) {
return 0;
}
const int spaceWidth = renderer.getSpaceWidth(fontId);
int totalWidth = 0;
for (size_t i = 0; i < words.size(); ++i) {
totalWidth += measureWordWidth(renderer, fontId, words[i], wordStyles[i]);
// Add a space before this word unless it's the first word or a continuation
if (i > 0 && !wordContinues[i]) {
totalWidth += spaceWidth;
}
}
return static_cast<uint16_t>(std::min(totalWidth, static_cast<int>(UINT16_MAX)));
} }

View File

@@ -3,46 +3,58 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <functional> #include <functional>
#include <list>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include "blocks/BlockStyle.h"
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
class GfxRenderer; class GfxRenderer;
class ParsedText { class ParsedText {
std::list<std::string> words; std::vector<std::string> words;
std::list<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
TextBlock::Style style; std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
std::vector<bool> forceBreakAfter; // true = mandatory line break after this word (e.g. <br> in table cells)
BlockStyle blockStyle;
bool extraParagraphSpacing; bool extraParagraphSpacing;
bool hyphenationEnabled; bool hyphenationEnabled;
void applyParagraphIndent(); void applyParagraphIndent();
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth, std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
std::vector<uint16_t>& wordWidths); std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
int spaceWidth, std::vector<uint16_t>& wordWidths); int spaceWidth, std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec);
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, int spaceWidth, const std::vector<uint16_t>& wordWidths, void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<size_t>& lineBreakIndices, const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine); const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId); std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
public: public:
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing, explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
const bool hyphenationEnabled = false) const BlockStyle& blockStyle = BlockStyle())
: style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {} : blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
~ParsedText() = default; ~ParsedText() = default;
void addWord(std::string word, EpdFontFamily::Style fontStyle); void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
void setStyle(const TextBlock::Style style) { this->style = style; }
TextBlock::Style getStyle() const { return style; } /// Mark a forced line break after the last word (e.g. for <br> within table cells).
/// If no words have been added yet, this is a no-op.
void addLineBreak();
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
BlockStyle& getBlockStyle() { return blockStyle; }
size_t size() const { return words.size(); } size_t size() const { return words.size(); }
bool isEmpty() const { return words.empty(); } bool isEmpty() const { return words.empty(); }
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth, void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
bool includeLastLine = true); bool includeLastLine = true);
/// Returns the "natural" width of the content if it were laid out on a single line
/// (sum of word widths + space widths between non-continuation words).
/// Used by table layout to determine column widths before line-breaking.
uint16_t getNaturalWidth(const GfxRenderer& renderer, int fontId) const;
}; };

View File

@@ -1,6 +1,7 @@
#include "Section.h" #include "Section.h"
#include <SDCardManager.h> #include <HalStorage.h>
#include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include "Page.h" #include "Page.h"
@@ -8,24 +9,24 @@
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace { namespace {
constexpr uint8_t SECTION_FILE_VERSION = 10; constexpr uint8_t SECTION_FILE_VERSION = 13;
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(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint32_t); sizeof(uint32_t);
} // namespace } // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) { uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
if (!file) { if (!file) {
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount); LOG_ERR("SCT", "File not open for writing page %d", pageCount);
return 0; return 0;
} }
const uint32_t position = file.position(); const uint32_t position = file.position();
if (!page->serialize(file)) { if (!page->serialize(file)) {
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount); LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
return 0; return 0;
} }
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount); LOG_DBG("SCT", "Page %d processed", pageCount);
pageCount++; pageCount++;
return position; return position;
@@ -33,15 +34,16 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled) { const uint16_t viewportHeight, const bool hyphenationEnabled,
const bool embeddedStyle) {
if (!file) { if (!file) {
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); LOG_DBG("SCT", "File not open for writing header");
return; return;
} }
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(uint32_t), sizeof(embeddedStyle) + sizeof(uint32_t),
"Header size mismatch"); "Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId); serialization::writePod(file, fontId);
@@ -51,14 +53,15 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, viewportWidth); serialization::writePod(file, viewportWidth);
serialization::writePod(file, viewportHeight); serialization::writePod(file, viewportHeight);
serialization::writePod(file, hyphenationEnabled); serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
} }
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled) { const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
if (!SdMan.openFileForRead("SCT", filePath, file)) { if (!Storage.openFileForRead("SCT", filePath, file)) {
return false; return false;
} }
@@ -68,7 +71,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, version); serialization::readPod(file, version);
if (version != SECTION_FILE_VERSION) { if (version != SECTION_FILE_VERSION) {
file.close(); file.close();
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version); LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
clearCache(); clearCache();
return false; return false;
} }
@@ -79,6 +82,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
bool fileExtraParagraphSpacing; bool fileExtraParagraphSpacing;
uint8_t fileParagraphAlignment; uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled; bool fileHyphenationEnabled;
bool fileEmbeddedStyle;
serialization::readPod(file, fileFontId); serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing); serialization::readPod(file, fileExtraParagraphSpacing);
@@ -86,13 +90,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileViewportWidth); serialization::readPod(file, fileViewportWidth);
serialization::readPod(file, fileViewportHeight); serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled); serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle);
if (fontId != fileFontId || lineCompression != fileLineCompression || if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled) { hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
file.close(); file.close();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); clearCache();
return false; return false;
} }
@@ -100,39 +105,37 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, pageCount); serialization::readPod(file, pageCount);
file.close(); file.close();
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount); LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
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 {
if (!SdMan.exists(filePath.c_str())) { if (!Storage.exists(filePath.c_str())) {
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis()); LOG_DBG("SCT", "Cache does not exist, no action needed");
return true; return true;
} }
if (!SdMan.remove(filePath.c_str())) { if (!Storage.remove(filePath.c_str())) {
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis()); LOG_ERR("SCT", "Failed to clear cache");
return false; return false;
} }
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis()); LOG_DBG("SCT", "Cache cleared successfully");
return true; return true;
} }
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const std::function<void()>& progressSetupFn, const std::function<void()>& popupFn) {
const std::function<void(int)>& progressFn) {
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
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 tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
// Create cache directory if it doesn't exist // Create cache directory if it doesn't exist
{ {
const auto sectionsDir = epub->getCachePath() + "/sections"; const auto sectionsDir = epub->getCachePath() + "/sections";
SdMan.mkdir(sectionsDir.c_str()); Storage.mkdir(sectionsDir.c_str());
} }
// Retry logic for SD card timing issues // Retry logic for SD card timing issues
@@ -140,17 +143,17 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
uint32_t fileSize = 0; uint32_t fileSize = 0;
for (int attempt = 0; attempt < 3 && !success; attempt++) { for (int attempt = 0; attempt < 3 && !success; attempt++) {
if (attempt > 0) { if (attempt > 0) {
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1); LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
delay(50); // Brief delay before retry delay(50); // Brief delay before retry
} }
// Remove any incomplete file from previous attempt before retrying // Remove any incomplete file from previous attempt before retrying
if (SdMan.exists(tmpHtmlPath.c_str())) { if (Storage.exists(tmpHtmlPath.c_str())) {
SdMan.remove(tmpHtmlPath.c_str()); Storage.remove(tmpHtmlPath.c_str());
} }
FsFile tmpHtml; FsFile tmpHtml;
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
continue; continue;
} }
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
@@ -158,44 +161,56 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
tmpHtml.close(); tmpHtml.close();
// If streaming failed, remove the incomplete file immediately // If streaming failed, remove the incomplete file immediately
if (!success && SdMan.exists(tmpHtmlPath.c_str())) { if (!success && Storage.exists(tmpHtmlPath.c_str())) {
SdMan.remove(tmpHtmlPath.c_str()); Storage.remove(tmpHtmlPath.c_str());
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis()); LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
} }
} }
if (!success) { if (!success) {
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis()); LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
return false; return false;
} }
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize); LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
// Only show progress bar for larger chapters where rendering overhead is worth it if (!Storage.openFileForWrite("SCT", filePath, file)) {
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
progressSetupFn();
}
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
return false; return false;
} }
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled); viewportHeight, hyphenationEnabled, embeddedStyle);
std::vector<uint32_t> lut = {}; std::vector<uint32_t> lut = {};
// Derive the content base directory and image cache path prefix for the parser
size_t lastSlash = localPath.find_last_of('/');
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
CssParser* cssParser = nullptr;
if (embeddedStyle) {
cssParser = epub->getCssParser();
if (cssParser) {
if (!cssParser->loadFromCache()) {
LOG_ERR("SCT", "Failed to load CSS from cache");
}
}
}
ChapterHtmlSlimParser visitor( ChapterHtmlSlimParser visitor(
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, viewportHeight, hyphenationEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
progressFn); embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
Hyphenator::setPreferredLanguage(epub->getLanguage()); Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();
SdMan.remove(tmpHtmlPath.c_str()); Storage.remove(tmpHtmlPath.c_str());
if (!success) { if (!success) {
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis()); LOG_ERR("SCT", "Failed to parse XML and build pages");
file.close(); file.close();
SdMan.remove(filePath.c_str()); Storage.remove(filePath.c_str());
if (cssParser) {
cssParser->clear();
}
return false; return false;
} }
@@ -211,9 +226,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
} }
if (hasFailedLutRecords) { if (hasFailedLutRecords) {
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis()); LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
file.close(); file.close();
SdMan.remove(filePath.c_str()); Storage.remove(filePath.c_str());
return false; return false;
} }
@@ -222,11 +237,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
serialization::writePod(file, pageCount); serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset); serialization::writePod(file, lutOffset);
file.close(); file.close();
if (cssParser) {
cssParser->clear();
}
return true; return true;
} }
std::unique_ptr<Page> Section::loadPageFromSectionFile() { std::unique_ptr<Page> Section::loadPageFromSectionFile() {
if (!SdMan.openFileForRead("SCT", filePath, file)) { if (!Storage.openFileForRead("SCT", filePath, file)) {
return nullptr; return nullptr;
} }

View File

@@ -15,7 +15,8 @@ class Section {
FsFile file; FsFile file;
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
bool embeddedStyle);
uint32_t onPageComplete(std::unique_ptr<Page> page); uint32_t onPageComplete(std::unique_ptr<Page> page);
public: public:
@@ -29,11 +30,10 @@ class Section {
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
~Section() = default; ~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); uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle);
bool clearCache() const; bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
const std::function<void()>& progressSetupFn = nullptr, const std::function<void()>& popupFn = nullptr);
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSectionFile();
}; };

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

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

View File

@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
class Block { class Block {
public: public:
virtual ~Block() = default; virtual ~Block() = default;
virtual void layout(GfxRenderer& renderer) = 0;
virtual BlockType getType() = 0; virtual BlockType getType() = 0;
virtual bool isEmpty() = 0; virtual bool isEmpty() = 0;
virtual void finish() {} virtual void finish() {}

View File

@@ -0,0 +1,97 @@
#pragma once
#include <cstdint>
#include "Epub/css/CssStyle.h"
/**
* BlockStyle - Block-level styling properties
*/
struct BlockStyle {
CssTextAlign alignment = CssTextAlign::Justify;
// Spacing (in pixels)
int16_t marginTop = 0;
int16_t marginBottom = 0;
int16_t marginLeft = 0;
int16_t marginRight = 0;
int16_t paddingTop = 0; // treated same as margin for rendering
int16_t paddingBottom = 0; // treated same as margin for rendering
int16_t paddingLeft = 0; // treated same as margin for rendering
int16_t paddingRight = 0; // treated same as margin for rendering
int16_t textIndent = 0;
bool textIndentDefined = false; // true if text-indent was explicitly set in CSS
bool textAlignDefined = false; // true if text-align was explicitly set in CSS
// Combined horizontal insets (margin + padding)
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
[[nodiscard]] int16_t totalHorizontalInset() const { return leftInset() + rightInset(); }
// Combine with another block style. Useful for parent -> child styles, where the child style should be
// applied on top of the parent's style to get the combined style.
BlockStyle getCombinedBlockStyle(const BlockStyle& child) const {
BlockStyle combinedBlockStyle;
combinedBlockStyle.marginTop = static_cast<int16_t>(child.marginTop + marginTop);
combinedBlockStyle.marginBottom = static_cast<int16_t>(child.marginBottom + marginBottom);
combinedBlockStyle.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
combinedBlockStyle.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
combinedBlockStyle.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
combinedBlockStyle.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
combinedBlockStyle.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
combinedBlockStyle.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
// Text indent: use child's if defined
if (child.textIndentDefined) {
combinedBlockStyle.textIndent = child.textIndent;
combinedBlockStyle.textIndentDefined = true;
} else {
combinedBlockStyle.textIndent = textIndent;
combinedBlockStyle.textIndentDefined = textIndentDefined;
}
// Text align: use child's if defined
if (child.textAlignDefined) {
combinedBlockStyle.alignment = child.alignment;
combinedBlockStyle.textAlignDefined = true;
} else {
combinedBlockStyle.alignment = alignment;
combinedBlockStyle.textAlignDefined = textAlignDefined;
}
return combinedBlockStyle;
}
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
// emSize is the current font line height, used for em/rem unit conversion
// paragraphAlignment is the user's paragraphAlignment setting preference
static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment,
const uint16_t viewportWidth = 0) {
BlockStyle blockStyle;
const float vw = viewportWidth;
// Resolve all CssLength values to pixels using the current font's em size and viewport width
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw);
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw);
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw);
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw);
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw);
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw);
// For textIndent: if it's a percentage we can't resolve (no viewport width),
// leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used
if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) {
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw);
blockStyle.textIndentDefined = true;
}
blockStyle.textAlignDefined = cssStyle.hasTextAlign();
// User setting overrides CSS, unless "Book's Style" alignment setting is selected
if (paragraphAlignment == CssTextAlign::None) {
blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify;
} else {
blockStyle.alignment = paragraphAlignment;
}
return blockStyle;
}
};

View File

@@ -0,0 +1,174 @@
#include "ImageBlock.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include <SDCardManager.h>
#include <Serialization.h>
#include "../converters/DitherUtils.h"
#include "../converters/ImageDecoderFactory.h"
// Cache file format:
// - uint16_t width
// - uint16_t height
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
: imagePath(imagePath), width(width), height(height) {}
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
namespace {
std::string getCachePath(const std::string& imagePath) {
// Replace extension with .pxc (pixel cache)
size_t dotPos = imagePath.rfind('.');
if (dotPos != std::string::npos) {
return imagePath.substr(0, dotPos) + ".pxc";
}
return imagePath + ".pxc";
}
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
int expectedHeight) {
FsFile cacheFile;
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
return false;
}
uint16_t cachedWidth, cachedHeight;
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
cacheFile.close();
return false;
}
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
int widthDiff = abs(cachedWidth - expectedWidth);
int heightDiff = abs(cachedHeight - expectedHeight);
if (widthDiff > 1 || heightDiff > 1) {
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
expectedHeight);
cacheFile.close();
return false;
}
// Use cached dimensions for rendering (they're the actual decoded size)
expectedWidth = cachedWidth;
expectedHeight = cachedHeight;
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
// Read and render row by row to minimize memory usage
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
if (!rowBuffer) {
LOG_ERR("IMG", "Failed to allocate row buffer");
cacheFile.close();
return false;
}
for (int row = 0; row < cachedHeight; row++) {
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
LOG_ERR("IMG", "Cache read error at row %d", row);
free(rowBuffer);
cacheFile.close();
return false;
}
int destY = y + row;
for (int col = 0; col < cachedWidth; col++) {
int byteIdx = col / 4;
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
}
}
free(rowBuffer);
cacheFile.close();
LOG_DBG("IMG", "Cache render complete");
return true;
}
} // namespace
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
const int screenWidth = renderer.getScreenWidth();
const int screenHeight = renderer.getScreenHeight();
// Bounds check render position using logical screen dimensions
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
screenHeight);
return;
}
// Try to render from cache first
std::string cachePath = getCachePath(imagePath);
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
return; // Successfully rendered from cache
}
// No cache - need to decode the image
// Check if image file exists
FsFile file;
if (!Storage.openFileForRead("IMG", imagePath, file)) {
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
return;
}
size_t fileSize = file.size();
file.close();
if (fileSize == 0) {
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
return;
}
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
RenderConfig config;
config.x = x;
config.y = y;
config.maxWidth = width;
config.maxHeight = height;
config.useGrayscale = true;
config.useDithering = true;
config.performanceMode = false;
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
config.cachePath = cachePath; // Enable caching during decode
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
if (!decoder) {
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
return;
}
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
if (!success) {
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
return;
}
LOG_DBG("IMG", "Decode successful");
}
bool ImageBlock::serialize(FsFile& file) {
serialization::writeString(file, imagePath);
serialization::writePod(file, width);
serialization::writePod(file, height);
return true;
}
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
std::string path;
serialization::readString(file, path);
int16_t w, h;
serialization::readPod(file, w);
serialization::readPod(file, h);
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <SdFat.h>
#include <memory>
#include <string>
#include "Block.h"
class ImageBlock final : public Block {
public:
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
~ImageBlock() override = default;
const std::string& getImagePath() const { return imagePath; }
int16_t getWidth() const { return width; }
int16_t getHeight() const { return height; }
bool imageExists() const;
BlockType getType() override { return IMAGE_BLOCK; }
bool isEmpty() override { return false; }
void render(GfxRenderer& renderer, const int x, const int y);
bool serialize(FsFile& file);
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
private:
std::string imagePath;
int16_t width;
int16_t height;
};

View File

@@ -1,33 +1,50 @@
#include "TextBlock.h" #include "TextBlock.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
// Validate iterator bounds before rendering // Validate iterator bounds before rendering
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size()); (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
return; return;
} }
auto wordIt = words.begin();
auto wordStylesIt = wordStyles.begin();
auto wordXposIt = wordXpos.begin();
for (size_t i = 0; i < words.size(); i++) { for (size_t i = 0; i < words.size(); i++) {
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt); const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i];
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
std::advance(wordIt, 1); if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
std::advance(wordStylesIt, 1); const std::string& w = words[i];
std::advance(wordXposIt, 1); const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
int startX = wordX;
int underlineWidth = fullWordWidth;
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
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* visiblePtr = w.c_str() + 3;
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83");
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
startX = wordX + prefixWidth;
underlineWidth = visibleWidth;
}
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
}
} }
} }
bool TextBlock::serialize(FsFile& file) const { bool TextBlock::serialize(FsFile& file) const {
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
words.size(), wordXpos.size(), wordStyles.size()); wordXpos.size(), wordStyles.size());
return false; return false;
} }
@@ -37,25 +54,36 @@ bool TextBlock::serialize(FsFile& file) const {
for (auto x : wordXpos) serialization::writePod(file, x); for (auto x : wordXpos) serialization::writePod(file, x);
for (auto s : wordStyles) serialization::writePod(file, s); for (auto s : wordStyles) serialization::writePod(file, s);
// Block style // Style (alignment + margins/padding/indent)
serialization::writePod(file, style); serialization::writePod(file, blockStyle.alignment);
serialization::writePod(file, blockStyle.textAlignDefined);
serialization::writePod(file, blockStyle.marginTop);
serialization::writePod(file, blockStyle.marginBottom);
serialization::writePod(file, blockStyle.marginLeft);
serialization::writePod(file, blockStyle.marginRight);
serialization::writePod(file, blockStyle.paddingTop);
serialization::writePod(file, blockStyle.paddingBottom);
serialization::writePod(file, blockStyle.paddingLeft);
serialization::writePod(file, blockStyle.paddingRight);
serialization::writePod(file, blockStyle.textIndent);
serialization::writePod(file, blockStyle.textIndentDefined);
return true; return true;
} }
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) { std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
uint16_t wc; uint16_t wc;
std::list<std::string> words; std::vector<std::string> words;
std::list<uint16_t> wordXpos; std::vector<uint16_t> wordXpos;
std::list<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
Style style; BlockStyle blockStyle;
// Word count // Word count
serialization::readPod(file, wc); serialization::readPod(file, wc);
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block) // Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
if (wc > 10000) { if (wc > 10000) {
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc); LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
return nullptr; return nullptr;
} }
@@ -67,8 +95,20 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
for (auto& x : wordXpos) serialization::readPod(file, x); for (auto& x : wordXpos) serialization::readPod(file, x);
for (auto& s : wordStyles) serialization::readPod(file, s); for (auto& s : wordStyles) serialization::readPod(file, s);
// Block style // Style (alignment + margins/padding/indent)
serialization::readPod(file, style); serialization::readPod(file, blockStyle.alignment);
serialization::readPod(file, blockStyle.textAlignDefined);
serialization::readPod(file, blockStyle.marginTop);
serialization::readPod(file, blockStyle.marginBottom);
serialization::readPod(file, blockStyle.marginLeft);
serialization::readPod(file, blockStyle.marginRight);
serialization::readPod(file, blockStyle.paddingTop);
serialization::readPod(file, blockStyle.paddingBottom);
serialization::readPod(file, blockStyle.paddingLeft);
serialization::readPod(file, blockStyle.paddingRight);
serialization::readPod(file, blockStyle.textIndent);
serialization::readPod(file, blockStyle.textIndentDefined);
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style)); return std::unique_ptr<TextBlock>(
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
} }

View File

@@ -1,38 +1,36 @@
#pragma once #pragma once
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <SdFat.h> #include <HalStorage.h>
#include <list>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "Block.h" #include "Block.h"
#include "BlockStyle.h"
// Represents a line of text on a page // Represents a line of text on a page
class TextBlock final : public Block { class TextBlock final : public Block {
public:
enum Style : uint8_t {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
private: private:
std::list<std::string> words; std::vector<std::string> words;
std::list<uint16_t> wordXpos; std::vector<uint16_t> wordXpos;
std::list<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
Style style; BlockStyle blockStyle;
public: public:
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
std::list<EpdFontFamily::Style> word_styles, const Style style) std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {} : words(std::move(words)),
wordXpos(std::move(word_xpos)),
wordStyles(std::move(word_styles)),
blockStyle(blockStyle) {}
~TextBlock() override = default; ~TextBlock() override = default;
void setStyle(const Style style) { this->style = style; } void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
Style getStyle() const { return style; } const BlockStyle& getBlockStyle() const { return blockStyle; }
const std::vector<std::string>& getWords() const { return words; }
const std::vector<uint16_t>& getWordXpos() const { return wordXpos; }
const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
bool isEmpty() override { return words.empty(); } bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines // given a renderer works out where to break the words into lines
void render(const GfxRenderer& renderer, int fontId, int x, int y) const; void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
BlockType getType() override { return TEXT_BLOCK; } BlockType getType() override { return TEXT_BLOCK; }

View File

@@ -0,0 +1,40 @@
#pragma once
#include <GfxRenderer.h>
#include <stdint.h>
// 4x4 Bayer matrix for ordered dithering
inline const uint8_t bayer4x4[4][4] = {
{0, 8, 2, 10},
{12, 4, 14, 6},
{3, 11, 1, 9},
{15, 7, 13, 5},
};
// Apply Bayer dithering and quantize to 4 levels (0-3)
// Stateless - works correctly with any pixel processing order
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
int bayer = bayer4x4[y & 3][x & 3];
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
int adjusted = gray + dither;
if (adjusted < 0) adjusted = 0;
if (adjusted > 255) adjusted = 255;
if (adjusted < 64) return 0;
if (adjusted < 128) return 1;
if (adjusted < 192) return 2;
return 3;
}
// Draw a pixel respecting the current render mode for grayscale support
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
renderer.drawPixel(x, y, true);
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
renderer.drawPixel(x, y, false);
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
renderer.drawPixel(x, y, false);
}
}

View File

@@ -0,0 +1,42 @@
#include "ImageDecoderFactory.h"
#include <Logging.h>
#include <memory>
#include <string>
#include "JpegToFramebufferConverter.h"
#include "PngToFramebufferConverter.h"
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
std::string ext = imagePath;
size_t dotPos = ext.rfind('.');
if (dotPos != std::string::npos) {
ext = ext.substr(dotPos);
for (auto& c : ext) {
c = tolower(c);
}
} else {
ext = "";
}
if (JpegToFramebufferConverter::supportsFormat(ext)) {
if (!jpegDecoder) {
jpegDecoder.reset(new JpegToFramebufferConverter());
}
return jpegDecoder.get();
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
if (!pngDecoder) {
pngDecoder.reset(new PngToFramebufferConverter());
}
return pngDecoder.get();
}
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
return nullptr;
}
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }

View File

@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "ImageToFramebufferDecoder.h"
class JpegToFramebufferConverter;
class PngToFramebufferConverter;
class ImageDecoderFactory {
public:
// Returns non-owning pointer - factory owns the decoder lifetime
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
static bool isFormatSupported(const std::string& imagePath);
private:
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
};

View File

@@ -0,0 +1,17 @@
#include "ImageToFramebufferDecoder.h"
#include <Logging.h>
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
if (width * height > MAX_SOURCE_PIXELS) {
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
format.c_str(), MAX_SOURCE_PIXELS);
return false;
}
return true;
}
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
imagePath.c_str());
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <SdFat.h>
#include <memory>
#include <string>
class GfxRenderer;
struct ImageDimensions {
int16_t width;
int16_t height;
};
struct RenderConfig {
int x, y;
int maxWidth, maxHeight;
bool useGrayscale = true;
bool useDithering = true;
bool performanceMode = false;
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
};
class ImageToFramebufferDecoder {
public:
virtual ~ImageToFramebufferDecoder() = default;
virtual bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) = 0;
virtual bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const = 0;
virtual const char* getFormatName() const = 0;
protected:
// Size validation helpers
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
bool validateImageDimensions(int width, int height, const std::string& format);
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
};

View File

@@ -0,0 +1,297 @@
#include "JpegToFramebufferConverter.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include <SDCardManager.h>
#include <SdFat.h>
#include <picojpeg.h>
#include <cstdio>
#include <cstring>
#include "DitherUtils.h"
#include "PixelCache.h"
struct JpegContext {
FsFile& file;
uint8_t buffer[512];
size_t bufferPos;
size_t bufferFilled;
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
};
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
FsFile file;
if (!Storage.openFileForRead("JPG", imagePath, file)) {
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
return false;
}
JpegContext context(file);
pjpeg_image_info_t imageInfo;
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
file.close();
if (status != 0) {
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
return false;
}
out.width = imageInfo.m_width;
out.height = imageInfo.m_height;
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
return true;
}
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
const RenderConfig& config) {
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
FsFile file;
if (!Storage.openFileForRead("JPG", imagePath, file)) {
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
return false;
}
JpegContext context(file);
pjpeg_image_info_t imageInfo;
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
if (status != 0) {
LOG_ERR("JPG", "picojpeg init failed: %d", status);
file.close();
return false;
}
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
file.close();
return false;
}
// Calculate output dimensions
int destWidth, destHeight;
float scale;
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
destWidth = config.maxWidth;
destHeight = config.maxHeight;
scale = (float)destWidth / imageInfo.m_width;
} else {
// Calculate scale factor to fit within maxWidth/maxHeight
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth)
? (float)config.maxWidth / imageInfo.m_width
: 1.0f;
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
? (float)config.maxHeight / imageInfo.m_height
: 1.0f;
scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale > 1.0f) scale = 1.0f;
destWidth = (int)(imageInfo.m_width * scale);
destHeight = (int)(imageInfo.m_height * scale);
}
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height,
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
file.close();
return false;
}
const int screenWidth = renderer.getScreenWidth();
const int screenHeight = renderer.getScreenHeight();
// Allocate pixel cache if cachePath is provided
PixelCache cache;
bool caching = !config.cachePath.empty();
if (caching) {
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
caching = false;
}
}
int mcuX = 0;
int mcuY = 0;
while (mcuY < imageInfo.m_MCUSPerCol) {
status = pjpeg_decode_mcu();
if (status == PJPG_NO_MORE_BLOCKS) {
break;
}
if (status != 0) {
LOG_ERR("JPG", "MCU decode failed: %d", status);
file.close();
return false;
}
// Source position in image coordinates
int srcStartX = mcuX * imageInfo.m_MCUWidth;
int srcStartY = mcuY * imageInfo.m_MCUHeight;
switch (imageInfo.m_scanType) {
case PJPG_GRAYSCALE:
for (int row = 0; row < 8; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 8; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH1V1:
for (int row = 0; row < 8; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 8; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH2V1:
for (int row = 0; row < 8; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 16; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
int blockIndex = (col < 8) ? 0 : 1;
int pixelIndex = row * 8 + (col % 8);
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH1V2:
for (int row = 0; row < 16; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 8; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
int blockIndex = (row < 8) ? 0 : 1;
int pixelIndex = (row % 8) * 8 + col;
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
case PJPG_YH2V2:
for (int row = 0; row < 16; row++) {
int srcY = srcStartY + row;
int destY = config.y + (int)(srcY * scale);
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
for (int col = 0; col < 16; col++) {
int srcX = srcStartX + col;
int destX = config.x + (int)(srcX * scale);
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
int blockX = (col < 8) ? 0 : 1;
int blockY = (row < 8) ? 0 : 1;
int blockIndex = blockY * 2 + blockX;
int pixelIndex = (row % 8) * 8 + (col % 8);
int blockOffset = blockIndex * 64;
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
if (dithered > 3) dithered = 3;
drawPixelWithRenderMode(renderer, destX, destY, dithered);
if (caching) cache.setPixel(destX, destY, dithered);
}
}
break;
}
mcuX++;
if (mcuX >= imageInfo.m_MCUSPerRow) {
mcuX = 0;
mcuY++;
}
}
LOG_DBG("JPG", "Decoding complete");
file.close();
// Write cache file if caching was enabled
if (caching) {
cache.writeToFile(config.cachePath);
}
return true;
}
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data) {
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
if (context->bufferPos >= context->bufferFilled) {
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
if (readCount <= 0) {
*pBytes_actually_read = 0;
return 0;
}
context->bufferFilled = readCount;
context->bufferPos = 0;
}
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
context->bufferPos += bytesToCopy;
*pBytes_actually_read = bytesToCopy;
return 0;
}
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension;
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".jpg" || ext == ".jpeg");
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <stdint.h>
#include <string>
#include "ImageToFramebufferDecoder.h"
class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
public:
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
return getDimensionsStatic(imagePath, dims);
}
static bool supportsFormat(const std::string& extension);
const char* getFormatName() const override { return "JPEG"; }
private:
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data);
};

View File

@@ -0,0 +1,82 @@
#pragma once
#include <HalStorage.h>
#include <Logging.h>
#include <stdint.h>
#include <cstring>
#include <string>
// Cache buffer for storing 2-bit pixels (4 levels) during decode.
// Packs 4 pixels per byte, MSB first.
struct PixelCache {
uint8_t* buffer;
int width;
int height;
int bytesPerRow;
int originX; // config.x - to convert screen coords to cache coords
int originY; // config.y
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
PixelCache(const PixelCache&) = delete;
PixelCache& operator=(const PixelCache&) = delete;
static constexpr size_t MAX_CACHE_BYTES = 256 * 1024; // 256KB limit for embedded targets
bool allocate(int w, int h, int ox, int oy) {
width = w;
height = h;
originX = ox;
originY = oy;
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
size_t bufferSize = (size_t)bytesPerRow * h;
if (bufferSize > MAX_CACHE_BYTES) {
LOG_ERR("IMG", "Cache buffer too large: %d bytes for %dx%d (limit %d)", bufferSize, w, h, MAX_CACHE_BYTES);
return false;
}
buffer = (uint8_t*)malloc(bufferSize);
if (buffer) {
memset(buffer, 0, bufferSize);
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
}
return buffer != nullptr;
}
void setPixel(int screenX, int screenY, uint8_t value) {
if (!buffer) return;
int localX = screenX - originX;
int localY = screenY - originY;
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
int byteIdx = localY * bytesPerRow + localX / 4;
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
}
bool writeToFile(const std::string& cachePath) {
if (!buffer) return false;
FsFile cacheFile;
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
return false;
}
uint16_t w = width;
uint16_t h = height;
cacheFile.write(&w, 2);
cacheFile.write(&h, 2);
cacheFile.write(buffer, bytesPerRow * height);
cacheFile.close();
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
return true;
}
~PixelCache() {
if (buffer) {
free(buffer);
buffer = nullptr;
}
}
};

View File

@@ -0,0 +1,362 @@
#include "PngToFramebufferConverter.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include <PNGdec.h>
#include <SDCardManager.h>
#include <SdFat.h>
#include <cstdlib>
#include <new>
#include "DitherUtils.h"
#include "PixelCache.h"
namespace {
// Context struct passed through PNGdec callbacks to avoid global mutable state.
// The draw callback receives this via pDraw->pUser (set by png.decode()).
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
struct PngContext {
GfxRenderer* renderer;
const RenderConfig* config;
int screenWidth;
int screenHeight;
// Scaling state
float scale;
int srcWidth;
int srcHeight;
int dstWidth;
int dstHeight;
int lastDstY; // Track last rendered destination Y to avoid duplicates
PixelCache cache;
bool caching;
uint8_t* grayLineBuffer;
PngContext()
: renderer(nullptr),
config(nullptr),
screenWidth(0),
screenHeight(0),
scale(1.0f),
srcWidth(0),
srcHeight(0),
dstWidth(0),
dstHeight(0),
lastDstY(-1),
caching(false),
grayLineBuffer(nullptr) {}
};
// File I/O callbacks use pFile->fHandle to access the FsFile*,
// avoiding the need for global file state.
void* pngOpenWithHandle(const char* filename, int32_t* size) {
FsFile* f = new FsFile();
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
delete f;
return nullptr;
}
*size = f->size();
return f;
}
void pngCloseWithHandle(void* handle) {
FsFile* f = reinterpret_cast<FsFile*>(handle);
if (f) {
f->close();
delete f;
}
}
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return 0;
return f->read(pBuf, len);
}
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return -1;
return f->seek(pos);
}
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
// We heap-allocate it on demand rather than using a static instance, so this memory
// is only consumed while actually decoding/querying PNG images. This is critical on
// the ESP32-C3 where total RAM is ~320 KB.
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
// Convert entire source line to grayscale with alpha blending to white background.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
switch (pixelType) {
case PNG_PIXEL_GRAYSCALE:
memcpy(grayLine, pPixels, width);
break;
case PNG_PIXEL_TRUECOLOR:
for (int x = 0; x < width; x++) {
uint8_t* p = &pPixels[x * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
}
break;
case PNG_PIXEL_INDEXED:
if (palette) {
if (hasAlpha) {
for (int x = 0; x < width; x++) {
uint8_t idx = pPixels[x];
uint8_t* p = &palette[idx * 3];
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = palette[768 + idx];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
} else {
for (int x = 0; x < width; x++) {
uint8_t* p = &palette[pPixels[x] * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
}
}
} else {
memcpy(grayLine, pPixels, width);
}
break;
case PNG_PIXEL_GRAY_ALPHA:
for (int x = 0; x < width; x++) {
uint8_t gray = pPixels[x * 2];
uint8_t alpha = pPixels[x * 2 + 1];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
break;
case PNG_PIXEL_TRUECOLOR_ALPHA:
for (int x = 0; x < width; x++) {
uint8_t* p = &pPixels[x * 4];
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = p[3];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
break;
default:
memset(grayLine, 128, width);
break;
}
}
int pngDrawCallback(PNGDRAW* pDraw) {
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
int srcY = pDraw->y;
int srcWidth = ctx->srcWidth;
// Calculate destination Y with scaling
int dstY = (int)(srcY * ctx->scale);
// Skip if we already rendered this destination row (multiple source rows map to same dest)
if (dstY == ctx->lastDstY) return 1;
ctx->lastDstY = dstY;
// Check bounds
if (dstY >= ctx->dstHeight) return 1;
int outY = ctx->config->y + dstY;
if (outY >= ctx->screenHeight) return 1;
// Convert entire source line to grayscale (improves cache locality)
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
pDraw->iHasAlpha);
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
int dstWidth = ctx->dstWidth;
int outXBase = ctx->config->x;
int screenWidth = ctx->screenWidth;
bool useDithering = ctx->config->useDithering;
bool caching = ctx->caching;
int srcX = 0;
int error = 0;
for (int dstX = 0; dstX < dstWidth; dstX++) {
int outX = outXBase + dstX;
if (outX < screenWidth) {
uint8_t gray = ctx->grayLineBuffer[srcX];
uint8_t ditheredGray;
if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
}
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
error += srcWidth;
while (error >= dstWidth) {
error -= dstWidth;
srcX++;
}
}
return 1;
}
} // namespace
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
return false;
}
PNG* png = new (std::nothrow) PNG();
if (!png) {
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
return false;
}
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
nullptr);
if (rc != 0) {
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
delete png;
return false;
}
out.width = png->getWidth();
out.height = png->getHeight();
png->close();
delete png;
return true;
}
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
const RenderConfig& config) {
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
return false;
}
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
PNG* png = new (std::nothrow) PNG();
if (!png) {
LOG_ERR("PNG", "Failed to allocate PNG decoder");
return false;
}
PngContext ctx;
ctx.renderer = &renderer;
ctx.config = &config;
ctx.screenWidth = renderer.getScreenWidth();
ctx.screenHeight = renderer.getScreenHeight();
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
pngDrawCallback);
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
delete png;
return false;
}
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
png->close();
delete png;
return false;
}
// Calculate output dimensions
ctx.srcWidth = png->getWidth();
ctx.srcHeight = png->getHeight();
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
ctx.dstWidth = config.maxWidth;
ctx.dstHeight = config.maxHeight;
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
} else {
// Calculate scale factor to fit within maxWidth/maxHeight
float scaleX = (float)config.maxWidth / ctx.srcWidth;
float scaleY = (float)config.maxHeight / ctx.srcHeight;
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
}
ctx.lastDstY = -1; // Reset row tracking
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
ctx.scale, png->getBpp());
if (png->getBpp() != 8) {
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
}
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
if (!ctx.grayLineBuffer) {
LOG_ERR("PNG", "Failed to allocate gray line buffer");
png->close();
delete png;
return false;
}
// Allocate cache buffer using SCALED dimensions
ctx.caching = !config.cachePath.empty();
if (ctx.caching) {
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
ctx.caching = false;
}
}
unsigned long decodeStart = millis();
rc = png->decode(&ctx, 0);
unsigned long decodeTime = millis() - decodeStart;
free(ctx.grayLineBuffer);
ctx.grayLineBuffer = nullptr;
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Decode failed: %d", rc);
png->close();
delete png;
return false;
}
png->close();
delete png;
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled and buffer was allocated
if (ctx.caching) {
ctx.cache.writeToFile(config.cachePath);
}
return true;
}
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension;
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".png");
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include "ImageToFramebufferDecoder.h"
class PngToFramebufferConverter final : public ImageToFramebufferDecoder {
public:
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
return getDimensionsStatic(imagePath, dims);
}
static bool supportsFormat(const std::string& extension);
const char* getFormatName() const override { return "PNG"; }
};

View File

@@ -0,0 +1,769 @@
#include "CssParser.h"
#include <Arduino.h>
#include <Logging.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <string_view>
namespace {
// Stack-allocated string buffer to avoid heap reallocations during parsing
// Provides string-like interface with fixed capacity
struct StackBuffer {
static constexpr size_t CAPACITY = 1024;
char data[CAPACITY];
size_t len = 0;
void push_back(char c) {
if (len < CAPACITY - 1) {
data[len++] = c;
}
}
void clear() { len = 0; }
bool empty() const { return len == 0; }
size_t size() const { return len; }
// Get string view of current content (zero-copy)
std::string_view view() const { return std::string_view(data, len); }
// Convert to string for passing to functions (single allocation)
std::string str() const { return std::string(data, len); }
};
// Buffer size for reading CSS files
constexpr size_t READ_BUFFER_SIZE = 512;
// Maximum number of CSS rules to store in the selector map
// Prevents unbounded memory growth from pathological CSS files
constexpr size_t MAX_RULES = 1500;
// Minimum free heap required to apply CSS during rendering
// If below this threshold, we skip CSS to avoid display artifacts.
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
// Maximum length for a single selector string
// Prevents parsing of extremely long or malformed selectors
constexpr size_t MAX_SELECTOR_LENGTH = 256;
// Check if character is CSS whitespace
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
} // anonymous namespace
// String utilities implementation
std::string CssParser::normalized(const std::string& s) {
std::string result;
result.reserve(s.size());
bool inSpace = true; // Start true to skip leading space
for (const char c : s) {
if (isCssWhitespace(c)) {
if (!inSpace) {
result.push_back(' ');
inSpace = true;
}
} else {
result.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
inSpace = false;
}
}
// Remove trailing space
if (!result.empty() && result.back() == ' ') {
result.pop_back();
}
return result;
}
void CssParser::normalizedInto(const std::string& s, std::string& out) {
out.clear();
out.reserve(s.size());
bool inSpace = true; // Start true to skip leading space
for (const char c : s) {
if (isCssWhitespace(c)) {
if (!inSpace) {
out.push_back(' ');
inSpace = true;
}
} else {
out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
inSpace = false;
}
}
if (!out.empty() && out.back() == ' ') {
out.pop_back();
}
}
std::vector<std::string> CssParser::splitOnChar(const std::string& s, const char delimiter) {
std::vector<std::string> parts;
size_t start = 0;
for (size_t i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == delimiter) {
std::string part = s.substr(start, i - start);
std::string trimmed = normalized(part);
if (!trimmed.empty()) {
parts.push_back(trimmed);
}
start = i + 1;
}
}
return parts;
}
std::vector<std::string> CssParser::splitWhitespace(const std::string& s) {
std::vector<std::string> parts;
size_t start = 0;
bool inWord = false;
for (size_t i = 0; i <= s.size(); ++i) {
const bool isSpace = i == s.size() || isCssWhitespace(s[i]);
if (isSpace && inWord) {
parts.push_back(s.substr(start, i - start));
inWord = false;
} else if (!isSpace && !inWord) {
start = i;
inWord = true;
}
}
return parts;
}
// Property value interpreters
CssTextAlign CssParser::interpretAlignment(const std::string& val) {
const std::string v = normalized(val);
if (v == "left" || v == "start") return CssTextAlign::Left;
if (v == "right" || v == "end") return CssTextAlign::Right;
if (v == "center") return CssTextAlign::Center;
if (v == "justify") return CssTextAlign::Justify;
return CssTextAlign::Left;
}
CssFontStyle CssParser::interpretFontStyle(const std::string& val) {
const std::string v = normalized(val);
if (v == "italic" || v == "oblique") return CssFontStyle::Italic;
return CssFontStyle::Normal;
}
CssFontWeight CssParser::interpretFontWeight(const std::string& val) {
const std::string v = normalized(val);
// Named values
if (v == "bold" || v == "bolder") return CssFontWeight::Bold;
if (v == "normal" || v == "lighter") return CssFontWeight::Normal;
// Numeric values: 100-900
// CSS spec: 400 = normal, 700 = bold
// We use: 0-400 = normal, 700+ = bold, 500-600 = normal (conservative)
char* endPtr = nullptr;
const long numericWeight = std::strtol(v.c_str(), &endPtr, 10);
// If we parsed a number and consumed the whole string
if (endPtr != v.c_str() && *endPtr == '\0') {
return numericWeight >= 700 ? CssFontWeight::Bold : CssFontWeight::Normal;
}
return CssFontWeight::Normal;
}
CssTextDecoration CssParser::interpretDecoration(const std::string& val) {
const std::string v = normalized(val);
// text-decoration can have multiple space-separated values
if (v.find("underline") != std::string::npos) {
return CssTextDecoration::Underline;
}
return CssTextDecoration::None;
}
CssLength CssParser::interpretLength(const std::string& val) {
const std::string v = normalized(val);
if (v.empty()) return CssLength{};
// Find where the number ends
size_t unitStart = v.size();
for (size_t i = 0; i < v.size(); ++i) {
const char c = v[i];
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
unitStart = i;
break;
}
}
const std::string numPart = v.substr(0, unitStart);
const std::string unitPart = v.substr(unitStart);
// Parse numeric value
char* endPtr = nullptr;
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
if (endPtr == numPart.c_str()) return CssLength{}; // No number parsed
// Determine unit type (preserve for deferred resolution)
auto unit = CssUnit::Pixels;
if (unitPart == "em") {
unit = CssUnit::Em;
} else if (unitPart == "rem") {
unit = CssUnit::Rem;
} else if (unitPart == "pt") {
unit = CssUnit::Points;
} else if (unitPart == "%") {
unit = CssUnit::Percent;
}
// px and unitless default to Pixels
return CssLength{numericValue, unit};
}
// Declaration parsing
void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf,
std::string& propValueBuf) {
const size_t colonPos = decl.find(':');
if (colonPos == std::string::npos || colonPos == 0) return;
normalizedInto(decl.substr(0, colonPos), propNameBuf);
normalizedInto(decl.substr(colonPos + 1), propValueBuf);
if (propNameBuf.empty() || propValueBuf.empty()) return;
if (propNameBuf == "text-align") {
style.textAlign = interpretAlignment(propValueBuf);
style.defined.textAlign = 1;
} else if (propNameBuf == "font-style") {
style.fontStyle = interpretFontStyle(propValueBuf);
style.defined.fontStyle = 1;
} else if (propNameBuf == "font-weight") {
style.fontWeight = interpretFontWeight(propValueBuf);
style.defined.fontWeight = 1;
} else if (propNameBuf == "text-decoration" || propNameBuf == "text-decoration-line") {
style.textDecoration = interpretDecoration(propValueBuf);
style.defined.textDecoration = 1;
} else if (propNameBuf == "text-indent") {
style.textIndent = interpretLength(propValueBuf);
style.defined.textIndent = 1;
} else if (propNameBuf == "margin-top") {
style.marginTop = interpretLength(propValueBuf);
style.defined.marginTop = 1;
} else if (propNameBuf == "margin-bottom") {
style.marginBottom = interpretLength(propValueBuf);
style.defined.marginBottom = 1;
} else if (propNameBuf == "margin-left") {
style.marginLeft = interpretLength(propValueBuf);
style.defined.marginLeft = 1;
} else if (propNameBuf == "margin-right") {
style.marginRight = interpretLength(propValueBuf);
style.defined.marginRight = 1;
} else if (propNameBuf == "margin") {
const auto values = splitWhitespace(propValueBuf);
if (!values.empty()) {
style.marginTop = interpretLength(values[0]);
style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop;
style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop;
style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight;
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
}
} else if (propNameBuf == "padding-top") {
style.paddingTop = interpretLength(propValueBuf);
style.defined.paddingTop = 1;
} else if (propNameBuf == "padding-bottom") {
style.paddingBottom = interpretLength(propValueBuf);
style.defined.paddingBottom = 1;
} else if (propNameBuf == "padding-left") {
style.paddingLeft = interpretLength(propValueBuf);
style.defined.paddingLeft = 1;
} else if (propNameBuf == "padding-right") {
style.paddingRight = interpretLength(propValueBuf);
style.defined.paddingRight = 1;
} else if (propNameBuf == "padding") {
const auto values = splitWhitespace(propValueBuf);
if (!values.empty()) {
style.paddingTop = interpretLength(values[0]);
style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop;
style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop;
style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight;
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
1;
}
} else if (propNameBuf == "width") {
style.width = interpretLength(propValueBuf);
style.defined.width = 1;
}
}
CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
CssStyle style;
std::string propNameBuf;
std::string propValueBuf;
size_t start = 0;
for (size_t i = 0; i <= declBlock.size(); ++i) {
if (i == declBlock.size() || declBlock[i] == ';') {
if (i > start) {
const size_t len = i - start;
std::string decl = declBlock.substr(start, len);
if (!decl.empty()) {
parseDeclarationIntoStyle(decl, style, propNameBuf, propValueBuf);
}
}
start = i + 1;
}
}
return style;
}
// Rule processing
void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style) {
// Check if we've reached the rule limit before processing
if (rulesBySelector_.size() >= MAX_RULES) {
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
return;
}
// Handle comma-separated selectors
const auto selectors = splitOnChar(selectorGroup, ',');
for (const auto& sel : selectors) {
// Validate selector length before processing
if (sel.size() > MAX_SELECTOR_LENGTH) {
LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH);
continue;
}
// Normalize the selector
std::string key = normalized(sel);
if (key.empty()) continue;
// Skip if this would exceed the rule limit
if (rulesBySelector_.size() >= MAX_RULES) {
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
return;
}
// Store or merge with existing
auto it = rulesBySelector_.find(key);
if (it != rulesBySelector_.end()) {
it->second.applyOver(style);
} else {
rulesBySelector_[key] = style;
}
}
}
// Main parsing entry point
bool CssParser::loadFromStream(FsFile& source) {
if (!source) {
LOG_ERR("CSS", "Cannot read from invalid file");
return false;
}
size_t totalRead = 0;
// Use stack-allocated buffers for parsing to avoid heap reallocations
StackBuffer selector;
StackBuffer declBuffer;
// Keep these as std::string since they're passed by reference to parseDeclarationIntoStyle
std::string propNameBuf;
std::string propValueBuf;
bool inComment = false;
bool maybeSlash = false;
bool prevStar = false;
bool inAtRule = false;
int atDepth = 0;
int bodyDepth = 0;
bool skippingRule = false;
CssStyle currentStyle;
auto handleChar = [&](const char c) {
if (inAtRule) {
if (c == '{') {
++atDepth;
} else if (c == '}') {
if (atDepth > 0) --atDepth;
if (atDepth == 0) inAtRule = false;
} else if (c == ';' && atDepth == 0) {
inAtRule = false;
}
return;
}
if (bodyDepth == 0) {
if (selector.empty() && isCssWhitespace(c)) {
return;
}
if (c == '@' && selector.empty()) {
inAtRule = true;
atDepth = 0;
return;
}
if (c == '{') {
bodyDepth = 1;
currentStyle = CssStyle{};
declBuffer.clear();
if (selector.size() > MAX_SELECTOR_LENGTH * 4) {
skippingRule = true;
}
return;
}
selector.push_back(c);
return;
}
// bodyDepth > 0
if (c == '{') {
++bodyDepth;
return;
}
if (c == '}') {
--bodyDepth;
if (bodyDepth == 0) {
if (!skippingRule && !declBuffer.empty()) {
parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf);
}
if (!skippingRule) {
processRuleBlockWithStyle(selector.str(), currentStyle);
}
selector.clear();
declBuffer.clear();
skippingRule = false;
return;
}
return;
}
if (bodyDepth > 1) {
return;
}
if (!skippingRule) {
if (c == ';') {
if (!declBuffer.empty()) {
parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf);
declBuffer.clear();
}
} else {
declBuffer.push_back(c);
}
}
};
char buffer[READ_BUFFER_SIZE];
while (source.available()) {
int bytesRead = source.read(buffer, sizeof(buffer));
if (bytesRead <= 0) break;
totalRead += static_cast<size_t>(bytesRead);
for (int i = 0; i < bytesRead; ++i) {
const char c = buffer[i];
if (inComment) {
if (prevStar && c == '/') {
inComment = false;
prevStar = false;
continue;
}
prevStar = c == '*';
continue;
}
if (maybeSlash) {
if (c == '*') {
inComment = true;
maybeSlash = false;
prevStar = false;
continue;
}
handleChar('/');
maybeSlash = false;
// fall through to process current char
}
if (c == '/') {
maybeSlash = true;
continue;
}
handleChar(c);
}
}
if (maybeSlash) {
handleChar('/');
}
LOG_DBG("CSS", "Parsed %zu rules from %zu bytes", rulesBySelector_.size(), totalRead);
return true;
}
// Style resolution
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
static bool lowHeapWarningLogged = false;
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
if (!lowHeapWarningLogged) {
lowHeapWarningLogged = true;
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
}
return CssStyle{};
}
CssStyle result;
const std::string tag = normalized(tagName);
// 1. Apply element-level style (lowest priority)
const auto tagIt = rulesBySelector_.find(tag);
if (tagIt != rulesBySelector_.end()) {
result.applyOver(tagIt->second);
}
// 2. Apply class styles (medium priority)
if (!classAttr.empty()) {
const auto classes = splitWhitespace(classAttr);
for (const auto& cls : classes) {
std::string classKey = "." + normalized(cls);
auto classIt = rulesBySelector_.find(classKey);
if (classIt != rulesBySelector_.end()) {
result.applyOver(classIt->second);
}
}
// 3. Apply element.class styles (higher priority)
for (const auto& cls : classes) {
std::string combinedKey = tag + "." + normalized(cls);
auto combinedIt = rulesBySelector_.find(combinedKey);
if (combinedIt != rulesBySelector_.end()) {
result.applyOver(combinedIt->second);
}
}
}
return result;
}
// Inline style parsing (static - doesn't need rule database)
CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return parseDeclarations(styleValue); }
// Cache serialization
// Cache format version - increment when format changes
constexpr uint8_t CSS_CACHE_VERSION = 2;
constexpr char rulesCache[] = "/css_rules.cache";
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
bool CssParser::saveToCache() const {
if (cachePath.empty()) {
return false;
}
FsFile file;
if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, file)) {
return false;
}
// Write version
file.write(CSS_CACHE_VERSION);
// Write rule count
const auto ruleCount = static_cast<uint16_t>(rulesBySelector_.size());
file.write(reinterpret_cast<const uint8_t*>(&ruleCount), sizeof(ruleCount));
// Write each rule: selector string + CssStyle fields
for (const auto& pair : rulesBySelector_) {
// Write selector string (length-prefixed)
const auto selectorLen = static_cast<uint16_t>(pair.first.size());
file.write(reinterpret_cast<const uint8_t*>(&selectorLen), sizeof(selectorLen));
file.write(reinterpret_cast<const uint8_t*>(pair.first.data()), selectorLen);
// Write CssStyle fields (all are POD types)
const CssStyle& style = pair.second;
file.write(static_cast<uint8_t>(style.textAlign));
file.write(static_cast<uint8_t>(style.fontStyle));
file.write(static_cast<uint8_t>(style.fontWeight));
file.write(static_cast<uint8_t>(style.textDecoration));
// Write CssLength fields (value + unit)
auto writeLength = [&file](const CssLength& len) {
file.write(reinterpret_cast<const uint8_t*>(&len.value), sizeof(len.value));
file.write(static_cast<uint8_t>(len.unit));
};
writeLength(style.textIndent);
writeLength(style.marginTop);
writeLength(style.marginBottom);
writeLength(style.marginLeft);
writeLength(style.marginRight);
writeLength(style.paddingTop);
writeLength(style.paddingBottom);
writeLength(style.paddingLeft);
writeLength(style.paddingRight);
// Write defined flags as uint16_t
uint16_t definedBits = 0;
if (style.defined.textAlign) definedBits |= 1 << 0;
if (style.defined.fontStyle) definedBits |= 1 << 1;
if (style.defined.fontWeight) definedBits |= 1 << 2;
if (style.defined.textDecoration) definedBits |= 1 << 3;
if (style.defined.textIndent) definedBits |= 1 << 4;
if (style.defined.marginTop) definedBits |= 1 << 5;
if (style.defined.marginBottom) definedBits |= 1 << 6;
if (style.defined.marginLeft) definedBits |= 1 << 7;
if (style.defined.marginRight) definedBits |= 1 << 8;
if (style.defined.paddingTop) definedBits |= 1 << 9;
if (style.defined.paddingBottom) definedBits |= 1 << 10;
if (style.defined.paddingLeft) definedBits |= 1 << 11;
if (style.defined.paddingRight) definedBits |= 1 << 12;
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
}
LOG_DBG("CSS", "Saved %u rules to cache", ruleCount);
file.close();
return true;
}
bool CssParser::loadFromCache() {
if (cachePath.empty()) {
return false;
}
FsFile file;
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
return false;
}
// Clear existing rules
clear();
// Read and verify version
uint8_t version = 0;
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) {
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION);
file.close();
return false;
}
// Read rule count
uint16_t ruleCount = 0;
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
file.close();
return false;
}
// Read each rule
for (uint16_t i = 0; i < ruleCount; ++i) {
// Read selector string
uint16_t selectorLen = 0;
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
rulesBySelector_.clear();
file.close();
return false;
}
std::string selector;
selector.resize(selectorLen);
if (file.read(&selector[0], selectorLen) != selectorLen) {
rulesBySelector_.clear();
file.close();
return false;
}
// Read CssStyle fields
CssStyle style;
uint8_t enumVal;
if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear();
file.close();
return false;
}
style.textAlign = static_cast<CssTextAlign>(enumVal);
if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear();
file.close();
return false;
}
style.fontStyle = static_cast<CssFontStyle>(enumVal);
if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear();
file.close();
return false;
}
style.fontWeight = static_cast<CssFontWeight>(enumVal);
if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear();
file.close();
return false;
}
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
// Read CssLength fields
auto readLength = [&file](CssLength& len) -> bool {
if (file.read(&len.value, sizeof(len.value)) != sizeof(len.value)) {
return false;
}
uint8_t unitVal;
if (file.read(&unitVal, 1) != 1) {
return false;
}
len.unit = static_cast<CssUnit>(unitVal);
return true;
};
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) {
rulesBySelector_.clear();
file.close();
return false;
}
// Read defined flags
uint16_t definedBits = 0;
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
rulesBySelector_.clear();
file.close();
return false;
}
style.defined.textAlign = (definedBits & 1 << 0) != 0;
style.defined.fontStyle = (definedBits & 1 << 1) != 0;
style.defined.fontWeight = (definedBits & 1 << 2) != 0;
style.defined.textDecoration = (definedBits & 1 << 3) != 0;
style.defined.textIndent = (definedBits & 1 << 4) != 0;
style.defined.marginTop = (definedBits & 1 << 5) != 0;
style.defined.marginBottom = (definedBits & 1 << 6) != 0;
style.defined.marginLeft = (definedBits & 1 << 7) != 0;
style.defined.marginRight = (definedBits & 1 << 8) != 0;
style.defined.paddingTop = (definedBits & 1 << 9) != 0;
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
rulesBySelector_[selector] = style;
}
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
file.close();
return true;
}

View File

@@ -0,0 +1,122 @@
#pragma once
#include <HalStorage.h>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "CssStyle.h"
/**
* Lightweight CSS parser for EPUB stylesheets
*
* Parses CSS files and extracts styling information relevant for e-ink display.
* Uses a two-phase approach: first tokenizes the CSS content, then builds
* a rule database that can be queried during HTML parsing.
*
* Supported selectors:
* - Element selectors: p, div, h1, etc.
* - Class selectors: .classname
* - Combined: element.classname
* - Grouped: selector1, selector2 { }
*
* Not supported (silently ignored):
* - Descendant/child selectors
* - Pseudo-classes and pseudo-elements
* - Media queries (content is skipped)
* - @import, @font-face, etc.
*/
class CssParser {
public:
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default;
// Non-copyable
CssParser(const CssParser&) = delete;
CssParser& operator=(const CssParser&) = delete;
/**
* Load and parse CSS from a file stream.
* Can be called multiple times to accumulate rules from multiple stylesheets.
* @param source Open file handle to read from
* @return true if parsing completed (even if no rules found)
*/
bool loadFromStream(FsFile& source);
/**
* Look up the style for an HTML element, considering tag name and class attributes.
* Applies CSS cascade: element style < class style < element.class style
*
* @param tagName The HTML element name (e.g., "p", "div")
* @param classAttr The class attribute value (may contain multiple space-separated classes)
* @return Combined style with all applicable rules merged
*/
[[nodiscard]] CssStyle resolveStyle(const std::string& tagName, const std::string& classAttr) const;
/**
* Parse an inline style attribute string.
* @param styleValue The value of a style="" attribute
* @return Parsed style properties
*/
[[nodiscard]] static CssStyle parseInlineStyle(const std::string& styleValue);
/**
* Check if any rules have been loaded
*/
[[nodiscard]] bool empty() const { return rulesBySelector_.empty(); }
/**
* Get count of loaded rule sets
*/
[[nodiscard]] size_t ruleCount() const { return rulesBySelector_.size(); }
/**
* Clear all loaded rules
*/
void clear() { rulesBySelector_.clear(); }
/**
* Check if CSS rules cache file exists
*/
bool hasCache() const;
/**
* Save parsed CSS rules to a cache file.
* @return true if cache was written successfully
*/
bool saveToCache() const;
/**
* Load CSS rules from a cache file.
* Clears any existing rules before loading.
* @return true if cache was loaded successfully
*/
bool loadFromCache();
private:
// Storage: maps normalized selector -> style properties
std::unordered_map<std::string, CssStyle> rulesBySelector_;
std::string cachePath;
// Internal parsing helpers
void processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style);
static CssStyle parseDeclarations(const std::string& declBlock);
static void parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf,
std::string& propValueBuf);
// Individual property value parsers
static CssTextAlign interpretAlignment(const std::string& val);
static CssFontStyle interpretFontStyle(const std::string& val);
static CssFontWeight interpretFontWeight(const std::string& val);
static CssTextDecoration interpretDecoration(const std::string& val);
static CssLength interpretLength(const std::string& val);
// String utilities
static std::string normalized(const std::string& s);
static void normalizedInto(const std::string& s, std::string& out);
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
static std::vector<std::string> splitWhitespace(const std::string& s);
};

View File

@@ -0,0 +1,212 @@
#pragma once
#include <cstdint>
// Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings
enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 };
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 };
// Represents a CSS length value with its unit, allowing deferred resolution to pixels
struct CssLength {
float value = 0.0f;
CssUnit unit = CssUnit::Pixels;
CssLength() = default;
CssLength(const float v, const CssUnit u) : value(v), unit(u) {}
// Convenience constructor for pixel values (most common case)
explicit CssLength(const float pixels) : value(pixels) {}
// Returns true if this length can be resolved to pixels with the given context.
// Percentage units require a non-zero containerWidth to resolve.
[[nodiscard]] bool isResolvable(const float containerWidth = 0) const {
return unit != CssUnit::Percent || containerWidth > 0;
}
// Resolve to pixels given the current em size (font line height)
// containerWidth is needed for percentage units (e.g. viewport width)
[[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const {
switch (unit) {
case CssUnit::Em:
case CssUnit::Rem:
return value * emSize;
case CssUnit::Points:
return value * 1.33f; // Approximate pt to px conversion
case CssUnit::Percent:
return value * containerWidth / 100.0f;
default:
return value;
}
}
// Resolve to int16_t pixels (for BlockStyle fields)
[[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const {
return static_cast<int16_t>(toPixels(emSize, containerWidth));
}
};
// Font style options matching CSS font-style property
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
// Font weight options - CSS supports 100-900, we simplify to normal/bold
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
// Text decoration options
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
// Bitmask for tracking which properties have been explicitly set
struct CssPropertyFlags {
uint16_t textAlign : 1;
uint16_t fontStyle : 1;
uint16_t fontWeight : 1;
uint16_t textDecoration : 1;
uint16_t textIndent : 1;
uint16_t marginTop : 1;
uint16_t marginBottom : 1;
uint16_t marginLeft : 1;
uint16_t marginRight : 1;
uint16_t paddingTop : 1;
uint16_t paddingBottom : 1;
uint16_t paddingLeft : 1;
uint16_t paddingRight : 1;
uint16_t width : 1;
CssPropertyFlags()
: textAlign(0),
fontStyle(0),
fontWeight(0),
textDecoration(0),
textIndent(0),
marginTop(0),
marginBottom(0),
marginLeft(0),
marginRight(0),
paddingTop(0),
paddingBottom(0),
paddingLeft(0),
paddingRight(0),
width(0) {}
[[nodiscard]] bool anySet() const {
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width;
}
void clearAll() {
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
marginTop = marginBottom = marginLeft = marginRight = 0;
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
width = 0;
}
};
// Represents a collection of CSS style properties
// Only stores properties relevant to e-ink text rendering
// Length values are stored as CssLength (value + unit) for deferred resolution
struct CssStyle {
CssTextAlign textAlign = CssTextAlign::Left;
CssFontStyle fontStyle = CssFontStyle::Normal;
CssFontWeight fontWeight = CssFontWeight::Normal;
CssTextDecoration textDecoration = CssTextDecoration::None;
CssLength textIndent; // First-line indent (deferred resolution)
CssLength marginTop; // Vertical spacing before block
CssLength marginBottom; // Vertical spacing after block
CssLength marginLeft; // Horizontal spacing left of block
CssLength marginRight; // Horizontal spacing right of block
CssLength paddingTop; // Padding before
CssLength paddingBottom; // Padding after
CssLength paddingLeft; // Padding left
CssLength paddingRight; // Padding right
CssLength width; // Element width (used for table columns/cells)
CssPropertyFlags defined; // Tracks which properties were explicitly set
// Apply properties from another style, only overwriting if the other style
// has that property explicitly defined
void applyOver(const CssStyle& base) {
if (base.hasTextAlign()) {
textAlign = base.textAlign;
defined.textAlign = 1;
}
if (base.hasFontStyle()) {
fontStyle = base.fontStyle;
defined.fontStyle = 1;
}
if (base.hasFontWeight()) {
fontWeight = base.fontWeight;
defined.fontWeight = 1;
}
if (base.hasTextDecoration()) {
textDecoration = base.textDecoration;
defined.textDecoration = 1;
}
if (base.hasTextIndent()) {
textIndent = base.textIndent;
defined.textIndent = 1;
}
if (base.hasMarginTop()) {
marginTop = base.marginTop;
defined.marginTop = 1;
}
if (base.hasMarginBottom()) {
marginBottom = base.marginBottom;
defined.marginBottom = 1;
}
if (base.hasMarginLeft()) {
marginLeft = base.marginLeft;
defined.marginLeft = 1;
}
if (base.hasMarginRight()) {
marginRight = base.marginRight;
defined.marginRight = 1;
}
if (base.hasPaddingTop()) {
paddingTop = base.paddingTop;
defined.paddingTop = 1;
}
if (base.hasPaddingBottom()) {
paddingBottom = base.paddingBottom;
defined.paddingBottom = 1;
}
if (base.hasPaddingLeft()) {
paddingLeft = base.paddingLeft;
defined.paddingLeft = 1;
}
if (base.hasPaddingRight()) {
paddingRight = base.paddingRight;
defined.paddingRight = 1;
}
if (base.hasWidth()) {
width = base.width;
defined.width = 1;
}
}
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
[[nodiscard]] bool hasFontStyle() const { return defined.fontStyle; }
[[nodiscard]] bool hasFontWeight() const { return defined.fontWeight; }
[[nodiscard]] bool hasTextDecoration() const { return defined.textDecoration; }
[[nodiscard]] bool hasTextIndent() const { return defined.textIndent; }
[[nodiscard]] bool hasMarginTop() const { return defined.marginTop; }
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
[[nodiscard]] bool hasMarginRight() const { return defined.marginRight; }
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
[[nodiscard]] bool hasWidth() const { return defined.width; }
void reset() {
textAlign = CssTextAlign::Left;
fontStyle = CssFontStyle::Normal;
fontWeight = CssFontWeight::Normal;
textDecoration = CssTextDecoration::None;
textIndent = CssLength{};
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
width = CssLength{};
defined.clearAll();
}
};

View File

@@ -0,0 +1,76 @@
// from
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
#include "htmlEntities.h"
#include <cstring>
struct EntityPair {
const char* key;
const char* value;
};
static const EntityPair ENTITY_LOOKUP[] = {
{"&quot;", "\""}, {"&frasl;", ""}, {"&amp;", "&"}, {"&lt;", "<"}, {"&gt;", ">"},
{"&Agrave;", "À"}, {"&Aacute;", "Á"}, {"&Acirc;", "Â"}, {"&Atilde;", "Ã"}, {"&Auml;", "Ä"},
{"&Aring;", "Å"}, {"&AElig;", "Æ"}, {"&Ccedil;", "Ç"}, {"&Egrave;", "È"}, {"&Eacute;", "É"},
{"&Ecirc;", "Ê"}, {"&Euml;", "Ë"}, {"&Igrave;", "Ì"}, {"&Iacute;", "Í"}, {"&Icirc;", "Î"},
{"&Iuml;", "Ï"}, {"&ETH;", "Ð"}, {"&Ntilde;", "Ñ"}, {"&Ograve;", "Ò"}, {"&Oacute;", "Ó"},
{"&Ocirc;", "Ô"}, {"&Otilde;", "Õ"}, {"&Ouml;", "Ö"}, {"&Oslash;", "Ø"}, {"&Ugrave;", "Ù"},
{"&Uacute;", "Ú"}, {"&Ucirc;", "Û"}, {"&Uuml;", "Ü"}, {"&Yacute;", "Ý"}, {"&THORN;", "Þ"},
{"&szlig;", "ß"}, {"&agrave;", "à"}, {"&aacute;", "á"}, {"&acirc;", "â"}, {"&atilde;", "ã"},
{"&auml;", "ä"}, {"&aring;", "å"}, {"&aelig;", "æ"}, {"&ccedil;", "ç"}, {"&egrave;", "è"},
{"&eacute;", "é"}, {"&ecirc;", "ê"}, {"&euml;", "ë"}, {"&igrave;", "ì"}, {"&iacute;", "í"},
{"&icirc;", "î"}, {"&iuml;", "ï"}, {"&eth;", "ð"}, {"&ntilde;", "ñ"}, {"&ograve;", "ò"},
{"&oacute;", "ó"}, {"&ocirc;", "ô"}, {"&otilde;", "õ"}, {"&ouml;", "ö"}, {"&oslash;", "ø"},
{"&ugrave;", "ù"}, {"&uacute;", "ú"}, {"&ucirc;", "û"}, {"&uuml;", "ü"}, {"&yacute;", "ý"},
{"&thorn;", "þ"}, {"&yuml;", "ÿ"}, {"&nbsp;", "\xC2\xA0"}, {"&iexcl;", "¡"}, {"&cent;", "¢"},
{"&pound;", "£"}, {"&curren;", "¤"}, {"&yen;", "¥"}, {"&brvbar;", "¦"}, {"&sect;", "§"},
{"&uml;", "¨"}, {"&copy;", "©"}, {"&ordf;", "ª"}, {"&laquo;", "«"}, {"&not;", "¬"},
{"&shy;", "­"}, {"&reg;", "®"}, {"&macr;", "¯"}, {"&deg;", "°"}, {"&plusmn;", "±"},
{"&sup2;", "²"}, {"&sup3;", "³"}, {"&acute;", "´"}, {"&micro;", "µ"}, {"&para;", ""},
{"&cedil;", "¸"}, {"&sup1;", "¹"}, {"&ordm;", "º"}, {"&raquo;", "»"}, {"&frac14;", "¼"},
{"&frac12;", "½"}, {"&frac34;", "¾"}, {"&iquest;", "¿"}, {"&times;", "×"}, {"&divide;", "÷"},
{"&forall;", ""}, {"&part;", ""}, {"&exist;", ""}, {"&empty;", ""}, {"&nabla;", ""},
{"&isin;", ""}, {"&notin;", ""}, {"&ni;", ""}, {"&prod;", ""}, {"&sum;", ""},
{"&minus;", ""}, {"&lowast;", ""}, {"&radic;", ""}, {"&prop;", ""}, {"&infin;", ""},
{"&ang;", ""}, {"&and;", ""}, {"&or;", ""}, {"&cap;", ""}, {"&cup;", ""},
{"&int;", ""}, {"&there4;", ""}, {"&sim;", ""}, {"&cong;", ""}, {"&asymp;", ""},
{"&ne;", ""}, {"&equiv;", ""}, {"&le;", ""}, {"&ge;", ""}, {"&sub;", ""},
{"&sup;", ""}, {"&nsub;", ""}, {"&sube;", ""}, {"&supe;", ""}, {"&oplus;", ""},
{"&otimes;", ""}, {"&perp;", ""}, {"&sdot;", ""}, {"&Alpha;", "Α"}, {"&Beta;", "Β"},
{"&Gamma;", "Γ"}, {"&Delta;", "Δ"}, {"&Epsilon;", "Ε"}, {"&Zeta;", "Ζ"}, {"&Eta;", "Η"},
{"&Theta;", "Θ"}, {"&Iota;", "Ι"}, {"&Kappa;", "Κ"}, {"&Lambda;", "Λ"}, {"&Mu;", "Μ"},
{"&Nu;", "Ν"}, {"&Xi;", "Ξ"}, {"&Omicron;", "Ο"}, {"&Pi;", "Π"}, {"&Rho;", "Ρ"},
{"&Sigma;", "Σ"}, {"&Tau;", "Τ"}, {"&Upsilon;", "Υ"}, {"&Phi;", "Φ"}, {"&Chi;", "Χ"},
{"&Psi;", "Ψ"}, {"&Omega;", "Ω"}, {"&alpha;", "α"}, {"&beta;", "β"}, {"&gamma;", "γ"},
{"&delta;", "δ"}, {"&epsilon;", "ε"}, {"&zeta;", "ζ"}, {"&eta;", "η"}, {"&theta;", "θ"},
{"&iota;", "ι"}, {"&kappa;", "κ"}, {"&lambda;", "λ"}, {"&mu;", "μ"}, {"&nu;", "ν"},
{"&xi;", "ξ"}, {"&omicron;", "ο"}, {"&pi;", "π"}, {"&rho;", "ρ"}, {"&sigmaf;", "ς"},
{"&sigma;", "σ"}, {"&tau;", "τ"}, {"&upsilon;", "υ"}, {"&phi;", "φ"}, {"&chi;", "χ"},
{"&psi;", "ψ"}, {"&omega;", "ω"}, {"&thetasym;", "ϑ"}, {"&upsih;", "ϒ"}, {"&piv;", "ϖ"},
{"&OElig;", "Œ"}, {"&oelig;", "œ"}, {"&Scaron;", "Š"}, {"&scaron;", "š"}, {"&Yuml;", "Ÿ"},
{"&fnof;", "ƒ"}, {"&circ;", "ˆ"}, {"&tilde;", "˜"}, {"&ensp;", ""}, {"&emsp;", ""},
{"&thinsp;", ""}, {"&zwnj;", ""}, {"&zwj;", ""}, {"&lrm;", ""}, {"&rlm;", ""},
{"&ndash;", ""}, {"&mdash;", ""}, {"&lsquo;", ""}, {"&rsquo;", ""}, {"&sbquo;", ""},
{"&ldquo;", ""}, {"&rdquo;", ""}, {"&bdquo;", ""}, {"&dagger;", ""}, {"&Dagger;", ""},
{"&bull;", ""}, {"&hellip;", ""}, {"&permil;", ""}, {"&prime;", ""}, {"&Prime;", ""},
{"&lsaquo;", ""}, {"&rsaquo;", ""}, {"&oline;", ""}, {"&euro;", ""}, {"&trade;", ""},
{"&larr;", ""}, {"&uarr;", ""}, {"&rarr;", ""}, {"&darr;", ""}, {"&harr;", ""},
{"&crarr;", ""}, {"&lceil;", ""}, {"&rceil;", ""}, {"&lfloor;", ""}, {"&rfloor;", ""},
{"&loz;", ""}, {"&spades;", ""}, {"&clubs;", ""}, {"&hearts;", ""}, {"&diams;", ""}};
static const size_t ENTITY_LOOKUP_COUNT = sizeof(ENTITY_LOOKUP) / sizeof(ENTITY_LOOKUP[0]);
// Lookup a single HTML entity and return its UTF-8 value
const char* lookupHtmlEntity(const char* entity, int len) {
for (size_t i = 0; i < ENTITY_LOOKUP_COUNT; i++) {
const char* key = ENTITY_LOOKUP[i].key;
const size_t keyLen = strlen(key);
if (static_cast<size_t>(len) == keyLen && memcmp(entity, key, keyLen) == 0) {
return ENTITY_LOOKUP[i].value;
}
}
return nullptr; // Entity not found
}

View File

@@ -0,0 +1,9 @@
// from
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
#pragma once
#include <string>
// Lookup a single HTML entity (including & and ;) and return its UTF-8 value
// Returns nullptr if entity is not found
const char* lookupHtmlEntity(const char* entity, int len);

View File

@@ -1,45 +1,84 @@
#include "LanguageRegistry.h" #include "LanguageRegistry.h"
#include <algorithm> #include <algorithm>
#include <array> #include <vector>
#include "HyphenationCommon.h" #include "HyphenationCommon.h"
#ifndef OMIT_HYPH_DE
#include "generated/hyph-de.trie.h" #include "generated/hyph-de.trie.h"
#endif
#ifndef OMIT_HYPH_EN
#include "generated/hyph-en.trie.h" #include "generated/hyph-en.trie.h"
#endif
#ifndef OMIT_HYPH_ES
#include "generated/hyph-es.trie.h" #include "generated/hyph-es.trie.h"
#endif
#ifndef OMIT_HYPH_FR
#include "generated/hyph-fr.trie.h" #include "generated/hyph-fr.trie.h"
#endif
#ifndef OMIT_HYPH_IT
#include "generated/hyph-it.trie.h"
#endif
#ifndef OMIT_HYPH_RU
#include "generated/hyph-ru.trie.h" #include "generated/hyph-ru.trie.h"
#endif
namespace { namespace {
#ifndef OMIT_HYPH_EN
// English hyphenation patterns (3/3 minimum prefix/suffix length) // English hyphenation patterns (3/3 minimum prefix/suffix length)
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3); LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
#endif
#ifndef OMIT_HYPH_FR
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
#endif
#ifndef OMIT_HYPH_DE
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic); #endif
#ifndef OMIT_HYPH_RU
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
#endif
#ifndef OMIT_HYPH_ES
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
#endif
#ifndef OMIT_HYPH_IT
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
#endif
using EntryArray = std::array<LanguageEntry, 5>; const LanguageEntryView entries() {
static const std::vector<LanguageEntry> kEntries = {
const EntryArray& entries() { #ifndef OMIT_HYPH_EN
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator}, {"english", "en", &englishHyphenator},
#endif
#ifndef OMIT_HYPH_FR
{"french", "fr", &frenchHyphenator}, {"french", "fr", &frenchHyphenator},
#endif
#ifndef OMIT_HYPH_DE
{"german", "de", &germanHyphenator}, {"german", "de", &germanHyphenator},
#endif
#ifndef OMIT_HYPH_RU
{"russian", "ru", &russianHyphenator}, {"russian", "ru", &russianHyphenator},
{"spanish", "es", &spanishHyphenator}}}; #endif
return kEntries; #ifndef OMIT_HYPH_ES
{"spanish", "es", &spanishHyphenator},
#endif
#ifndef OMIT_HYPH_IT
{"italian", "it", &italianHyphenator},
#endif
};
static const LanguageEntryView view{kEntries.data(), kEntries.size()};
return view;
} }
} // namespace } // namespace
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) { const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) {
const auto& allEntries = entries(); const auto allEntries = entries();
const auto it = std::find_if(allEntries.begin(), allEntries.end(), const auto it = std::find_if(allEntries.begin(), allEntries.end(),
[&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; }); [&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; });
return (it != allEntries.end()) ? it->hyphenator : nullptr; return (it != allEntries.end()) ? it->hyphenator : nullptr;
} }
LanguageEntryView getLanguageEntries() { LanguageEntryView getLanguageEntries() {
const auto& allEntries = entries(); return entries();
return LanguageEntryView{allEntries.data(), allEntries.size()};
} }

View File

@@ -53,6 +53,8 @@
namespace { namespace {
using EmbeddedAutomaton = SerializedHyphenationPatterns;
struct AugmentedWord { struct AugmentedWord {
std::vector<uint8_t> bytes; std::vector<uint8_t> bytes;
std::vector<size_t> charByteOffsets; std::vector<size_t> charByteOffsets;
@@ -141,59 +143,10 @@ struct AutomatonState {
bool valid() const { return data != nullptr; } bool valid() const { return data != nullptr; }
}; };
// Lightweight descriptor for the entire embedded automaton.
// The blob format is:
// [0..3] - big-endian root offset
// [4....] - node heap containing variable-sized headers + transition data
struct EmbeddedAutomaton {
const uint8_t* data = nullptr;
size_t size = 0;
uint32_t rootOffset = 0;
bool valid() const { return data != nullptr && size >= 4 && rootOffset < size; }
};
// Decode the serialized automaton header and root offset.
EmbeddedAutomaton parseAutomaton(const SerializedHyphenationPatterns& patterns) {
EmbeddedAutomaton automaton;
if (!patterns.data || patterns.size < 4) {
return automaton;
}
automaton.data = patterns.data;
automaton.size = patterns.size;
automaton.rootOffset = (static_cast<uint32_t>(patterns.data[0]) << 24) |
(static_cast<uint32_t>(patterns.data[1]) << 16) |
(static_cast<uint32_t>(patterns.data[2]) << 8) | static_cast<uint32_t>(patterns.data[3]);
if (automaton.rootOffset >= automaton.size) {
automaton.data = nullptr;
automaton.size = 0;
}
return automaton;
}
// Cache parsed automata per blob pointer to avoid reparsing.
const EmbeddedAutomaton& getAutomaton(const SerializedHyphenationPatterns& patterns) {
struct CacheEntry {
const SerializedHyphenationPatterns* key;
EmbeddedAutomaton automaton;
};
static std::vector<CacheEntry> cache;
for (const auto& entry : cache) {
if (entry.key == &patterns) {
return entry.automaton;
}
}
cache.push_back({&patterns, parseAutomaton(patterns)});
return cache.back().automaton;
}
// Interpret the node located at `addr`, returning transition metadata. // Interpret the node located at `addr`, returning transition metadata.
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) { AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
AutomatonState state; AutomatonState state;
if (!automaton.valid() || addr >= automaton.size) { if (addr >= automaton.size) {
return state; return state;
} }
@@ -234,7 +187,7 @@ AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
if (offset + levelsLen > automaton.size) { if (offset + levelsLen > automaton.size) {
return AutomatonState{}; return AutomatonState{};
} }
levelsPtr = automaton.data + offset; levelsPtr = automaton.data + offset - 4u;
} }
if (pos + childCount > remaining) { if (pos + childCount > remaining) {
@@ -344,10 +297,7 @@ std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
return {}; return {};
} }
const EmbeddedAutomaton& automaton = getAutomaton(patterns); const EmbeddedAutomaton& automaton = patterns;
if (!automaton.valid()) {
return {};
}
const AutomatonState root = decodeState(automaton, automaton.rootOffset); const AutomatonState root = decodeState(automaton, automaton.rootOffset);
if (!root.valid()) { if (!root.valid()) {

View File

@@ -5,6 +5,7 @@
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash. // Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
struct SerializedHyphenationPatterns { struct SerializedHyphenationPatterns {
size_t rootOffset;
const std::uint8_t* data; const std::uint8_t* data;
size_t size; size_t size;
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,377 +7,447 @@
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually. // Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t fr_trie_data[] = { alignas(4) constexpr uint8_t fr_trie_data[] = {
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x2B,
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17,
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x04, 0x1F, 0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36,
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C,
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B,
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x48, 0x2C, 0x0B, 0x29, 0x16,
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x3E, 0x0D,
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B,
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0x16, 0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD,
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21,
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63,
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD,
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0,
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x21, 0x6E, 0xFD, 0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61,
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA,
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD,
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x5E, 0x21, 0x74, 0xFC,
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x74,
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70,
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0x73, 0x72, 0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF,
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB,
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0,
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0x6E, 0x75, 0xF2, 0xFD, 0x21,
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0xFF, 0x06,
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21,
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0, 0x74, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2,
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41, 0x02, 0x52, 0x6E, 0x75, 0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD,
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70,
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD,
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x72, 0xFD, 0x21,
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96,
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x96, 0x41, 0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52,
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05,
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75,
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0xC2, 0x02,
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74,
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF,
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0xF4, 0xFF, 0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7,
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD,
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21,
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xFD, 0xA0, 0x00, 0x61,
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0xFE, 0xC8,
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43,
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E,
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0x70, 0x73, 0x72, 0x67, 0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D,
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2,
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x47, 0xFE, 0x47,
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43,
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x63, 0x74, 0x75, 0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F,
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0,
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75,
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0x6C,
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD,
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69, 0xA0, 0x04, 0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21,
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF, 0xC3, 0xFD, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD,
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02,
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x31, 0x21,
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41,
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44, 0x61, 0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41,
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x6F, 0xFE, 0x7B, 0xA0, 0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21,
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63,
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3,
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x74,
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75, 0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E,
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x72, 0x73, 0xFF, 0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9,
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C,
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1,
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x23, 0x21, 0x6E, 0xFD,
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0xFF,
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF,
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xFD, 0x44, 0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9,
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64,
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C,
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x68, 0xFC, 0x92, 0x23, 0x61,
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0xFC, 0x5A,
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79,
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0x6F, 0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69,
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0x6F, 0x73, 0x74, 0x75, 0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0,
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE,
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63,
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x6E, 0xFB, 0x41,
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21,
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D, 0xC3, 0xFC, 0x21, 0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3,
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64,
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD,
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xFE, 0xCA, 0x21, 0x6F,
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0x44,
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5,
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD,
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF, 0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21,
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xFA, 0xA4, 0xFA, 0xA4, 0xFF,
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0xFD, 0x44,
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5,
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41,
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0xA9, 0xFC, 0x27, 0x21, 0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21,
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2,
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73,
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0x21, 0xA9,
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4,
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0xFC, 0xBD, 0x21, 0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93,
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9,
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD,
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xA0, 0x01, 0xC1, 0x21,
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x81,
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41,
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x73, 0xFE, 0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD,
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0x43, 0x6F, 0x73, 0x75, 0xFF, 0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD,
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0x61, 0xFE, 0xA9, 0x21, 0x69,
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0x21, 0x74,
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD,
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0x25, 0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB,
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42, 0xFD, 0x21, 0x61, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD,
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68,
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA,
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xFB,
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7,
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8, 0xFF, 0xFD, 0x41, 0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C,
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80,
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9,
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0xD7, 0xFF, 0xE4, 0xFD,
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xB9,
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21,
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6, 0xA9, 0xFD, 0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9,
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x42, 0x66, 0x78, 0xFB, 0x18, 0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1,
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41,
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x63, 0x68, 0x75, 0x6F, 0xFF,
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0xFD, 0xC3,
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43,
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x63, 0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F,
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41,
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0xF8, 0xFF, 0xF9,
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41,
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0x69, 0xF7, 0xD2, 0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73,
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF,
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70,
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x41, 0x75, 0xF8, 0x30,
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0xF8,
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41,
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x73, 0xF8, 0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41,
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F, 0x69, 0xF8, 0x73, 0x21, 0x75, 0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA,
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72,
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0xBF, 0xF6, 0xBF, 0x42, 0x63,
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x74,
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2,
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF,
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21, 0xF9, 0xF6, 0x99, 0xF6, 0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF,
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF,
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xC3, 0x62, 0x63, 0x64, 0x65, 0x69,
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0xB1, 0xF8, 0xE6,
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65, 0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85,
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21, 0xF8, 0x85, 0xA0, 0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21,
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73,
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21,
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F,
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x61,
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74,
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74,
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD,
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0xE1, 0xFD, 0x41, 0x74, 0xFE,
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xF6, 0xFD,
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD,
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41,
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0x2E, 0xFF, 0x33, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63,
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22,
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74,
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E,
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD,
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64,
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6,
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xC9, 0xFF, 0xD4,
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02,
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x21, 0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1,
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69, 0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2,
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74,
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x00, 0xE2, 0x65, 0x75, 0xFF,
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0x62, 0xFF,
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43,
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC, 0x65, 0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92,
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65,
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63,
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD,
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0xE7, 0xFF, 0xF6,
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9, 0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21,
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21,
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC,
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48,
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFF, 0xA8, 0xFF, 0xBF,
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x8D,
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75,
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFD, 0x41, 0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70,
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0xFF, 0xFD, 0x42, 0x2E, 0x73,
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x74, 0xFD,
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F,
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0xE2, 0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21,
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70,
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE,
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41,
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xE2, 0x2E, 0x62,
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D,
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0xFC, 0xEE, 0xA0, 0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5,
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72,
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD,
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0x72, 0xFC, 0x41, 0x69,
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42,
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4,
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0x21, 0x69, 0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF,
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC,
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7,
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF,
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0x88, 0xA1,
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69,
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0, 0xFC, 0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE,
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0x8A, 0xFD, 0x27, 0xFD, 0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF,
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1,
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC,
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0xF2, 0x5A, 0xA1,
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22, 0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2,
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xF5, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0,
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65,
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1,
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFB, 0x41, 0xFF, 0xFB,
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0xFC,
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73,
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0xFB, 0x34, 0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69,
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08,
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54,
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xF3,
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0xFF, 0xFC,
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5,
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF,
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFC, 0xFB, 0x62, 0x42, 0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E,
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C,
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD,
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x68, 0xF8, 0xC0,
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A,
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61, 0xC3, 0x69, 0x63, 0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39,
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03,
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A,
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x76,
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0x73,
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D,
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2,
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x69, 0x75, 0xC3, 0x6F, 0x65, 0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41,
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9,
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFC, 0x3E, 0xFC, 0x3E, 0x41,
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0xFF, 0xFC,
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21,
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45, 0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C,
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0xEB, 0xFD, 0x42, 0xA9, 0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F,
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D,
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21,
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0x65, 0xF9, 0x7E,
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21,
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x75, 0xFC, 0xA0, 0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00,
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF,
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB,
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x07, 0x62, 0x21, 0xA9,
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0xFA,
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35,
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6,
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x41, 0x75, 0xF8, 0xC2, 0x22, 0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC,
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D,
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x74, 0x79, 0xFE, 0xAE, 0xFE,
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xC2, 0xFF,
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64,
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0xF1, 0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC,
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4, 0x41, 0x6C, 0xF1, 0x8F, 0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21,
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42,
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD,
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0xB5, 0xBC, 0xCE,
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF, 0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61,
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69, 0xFC, 0x22, 0x63, 0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD,
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1,
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70,
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x72, 0xF7, 0x21, 0x68,
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFA,
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD,
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21, 0x41, 0x61, 0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72,
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC,
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69,
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xFF, 0xA5,
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21,
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0x61, 0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA,
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0xFF, 0xDF, 0xFF, 0xEB, 0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61,
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63,
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6,
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0x41, 0x70, 0xED,
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x6D, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70,
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD,
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21,
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79,
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xFD,
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72,
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF6, 0xA6, 0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74,
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xF0, 0x03, 0xFF, 0xFC, 0x45, 0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD,
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61,
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0xFB, 0x9D, 0x21, 0x68, 0xFC,
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xFB, 0xEE,
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41,
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD, 0x6D, 0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1,
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2,
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21,
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7,
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xFF, 0xF3, 0xF7,
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1, 0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00,
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE1, 0x6D, 0x72, 0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65,
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3,
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5,
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0x41,
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x2E,
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20,
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0xFF, 0x4D, 0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC,
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75,
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0x32, 0xFF, 0xFD, 0xC2, 0x00,
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0x65, 0x69,
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD,
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xC4, 0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD,
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0xC4, 0xF4, 0xD1, 0x45, 0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4,
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD,
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2,
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0x61, 0xC3, 0x65,
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4,
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0x79, 0x41, 0x69, 0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5,
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5,
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21,
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0x6F, 0x73, 0xF5, 0x12,
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0x70,
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00,
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66, 0xE2, 0x75, 0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD,
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0, 0x41, 0x6D, 0xF4, 0x02, 0x21, 0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22,
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0,
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0xFF, 0xFC, 0xF3, 0xDA, 0x41,
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xFB, 0x17,
0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6,
0x66, 0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29,
0xC6, 0x00, 0xE2, 0x2E, 0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF,
0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21,
0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA9,
0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65,
0xF3, 0x44, 0x21, 0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9,
0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5,
0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F,
0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x41, 0x69, 0xF3, 0x97,
0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0x6E,
0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41,
0x6F, 0xF6, 0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62,
0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F,
0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21,
0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF7, 0x81, 0x21, 0x72, 0xFC,
0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF7, 0x6F,
0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21,
0x63, 0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC,
0xDC, 0xFB, 0x21, 0x74, 0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D,
0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D,
0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E,
0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x72, 0xFF, 0xFB,
0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1,
0x00, 0xE1, 0x6D, 0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB,
0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06,
0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD,
0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x22,
0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75,
0x79, 0xF1, 0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41,
0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC,
0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73,
0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x6E, 0x73, 0xE9, 0xBA, 0xE9,
0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x00, 0xE1,
0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41,
0x74, 0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF,
0xFD, 0xF2, 0x88, 0x42, 0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73,
0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD,
0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0x41, 0x6D,
0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB,
0xF2, 0x50, 0xF0, 0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42,
0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48,
0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6,
0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0x21, 0x74, 0xFC, 0x21,
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0x6D,
0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80,
0xF0, 0x80, 0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1,
0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0,
0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0,
0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0x41, 0x69, 0xFA,
0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0xFD, 0x22,
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69,
0x6F, 0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0,
0x0B, 0xF4, 0x0D, 0xFF, 0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC,
0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61,
0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F,
0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB,
0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0,
0xEF, 0x48, 0xF0, 0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F,
0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4,
0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65,
0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
}; };
constexpr SerializedHyphenationPatterns fr_patterns = { constexpr SerializedHyphenationPatterns fr_patterns = {
0x1AF0u,
fr_trie_data, fr_trie_data,
sizeof(fr_trie_data), sizeof(fr_trie_data),
}; };

View File

@@ -0,0 +1,113 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "../SerializedHyphenationTrie.h"
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t it_trie_data[] = {
0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x0D, 0x16, 0x0B, 0x34,
0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x01, 0x02, 0x16, 0x02,
0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0B, 0xA0, 0x00, 0x42,
0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x6D, 0xFD, 0x21, 0x69,
0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0x21, 0x6F, 0xFD, 0x21,
0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x6D,
0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6F, 0xFD,
0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x61, 0x69, 0x6F, 0xDF,
0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63,
0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x75, 0xFD, 0x21,
0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x00,
0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0xA0, 0x01, 0x52, 0x21,
0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x71, 0x21, 0x6F,
0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x6F, 0xFD,
0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x72,
0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0x6C, 0x72, 0xFD, 0xFD,
0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x11, 0x25, 0x61, 0x68,
0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x72, 0xFD, 0x21, 0x63,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xA2, 0x21, 0x65, 0xFD,
0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x72, 0xFF, 0xFC, 0xFF,
0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x72,
0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0x21,
0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0xFD, 0x41, 0x6E, 0xFF,
0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x61, 0x65,
0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x12, 0xFF, 0x20, 0xFF,
0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0xC2, 0xFF, 0xE6, 0xFF,
0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xA0, 0x00, 0xD1, 0x24,
0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0xF1, 0xA0, 0x01,
0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xFD, 0x21, 0x75, 0xDF,
0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0x02, 0x01, 0x62, 0x63,
0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0xE3, 0xE3, 0xE3, 0xE3,
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0x27, 0xC4, 0xC7, 0xC6,
0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xFB, 0xFF, 0xBF,
0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x6E, 0x71, 0x73,
0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xEB, 0xFF,
0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0x67, 0x6C, 0x6D, 0x6E,
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0x72, 0x73, 0x74, 0x2E,
0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x4A, 0xFF,
0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0xFD, 0xD1, 0x02, 0x01,
0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E,
0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x33, 0xFF, 0x21, 0xFF,
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x68, 0x69, 0x6C, 0x6D,
0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0xFC, 0xFE, 0xF9, 0xFE,
0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0x02, 0x01, 0x2E, 0x27,
0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0x6D, 0x72, 0x73, 0x74,
0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0x2E, 0x27, 0xFE, 0x93,
0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0x21, 0x72, 0xF8, 0x21,
0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x69, 0xFC, 0x21, 0x65,
0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x82,
0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xFD, 0xCB, 0x02, 0x01,
0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0xB1, 0xFD, 0xC3, 0xFD,
0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD,
0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0x8D, 0xA0, 0x02, 0x53,
0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x68, 0x67, 0x6B, 0x6C,
0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x27, 0xFD, 0x79, 0xFD,
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x37, 0xFD, 0x37, 0xFD,
0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x8F, 0x4B, 0x62, 0x63,
0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xA0,
0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x70, 0x74, 0x7A, 0x2E,
0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0xF8, 0xFF, 0xFB, 0xC1,
0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x63, 0xFC, 0xC1, 0x01,
0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0x06, 0xD2, 0x02, 0x01,
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A,
0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xE2, 0xFC, 0xD3,
0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1,
0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0x76, 0x2E, 0x27, 0xFC,
0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x72, 0xFB, 0xAF, 0xA0,
0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0xFF, 0xF9, 0xFF, 0xFD,
0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0x70, 0x74, 0x77, 0x2E,
0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0xCB, 0x02, 0x01, 0x62,
0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFD, 0x9F,
0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0xC2, 0xFB, 0xF9, 0xFC,
0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0xC4, 0xFC, 0xED, 0xFD,
0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x5D, 0xFE, 0x81, 0xFE,
0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xD5, 0xFF, 0xDC,
};
constexpr SerializedHyphenationPatterns it_patterns = {
0x5C0u,
it_trie_data,
sizeof(it_trie_data),
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,26 +7,35 @@
#include <memory> #include <memory>
#include "../ParsedText.h" #include "../ParsedText.h"
#include "../TableData.h"
#include "../blocks/ImageBlock.h"
#include "../blocks/TextBlock.h" #include "../blocks/TextBlock.h"
#include "../css/CssParser.h"
#include "../css/CssStyle.h"
class Page; class Page;
class PageTableRow;
class GfxRenderer; class GfxRenderer;
class Epub;
#define MAX_WORD_SIZE 200 #define MAX_WORD_SIZE 200
class ChapterHtmlSlimParser { class ChapterHtmlSlimParser {
std::shared_ptr<Epub> epub;
const std::string& filepath; const std::string& filepath;
GfxRenderer& renderer; GfxRenderer& renderer;
std::function<void(std::unique_ptr<Page>)> completePageFn; std::function<void(std::unique_ptr<Page>)> completePageFn;
std::function<void(int)> progressFn; // Progress callback (0-100) std::function<void()> popupFn; // Popup callback
int depth = 0; int depth = 0;
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] = {};
int partWordBufferIndex = 0; int partWordBufferIndex = 0;
bool nextWordContinues = false; // true when next flushed word attaches to previous (inline element boundary)
std::unique_ptr<ParsedText> currentTextBlock = nullptr; std::unique_ptr<ParsedText> currentTextBlock = nullptr;
std::unique_ptr<Page> currentPage = nullptr; std::unique_ptr<Page> currentPage = nullptr;
int16_t currentPageNextY = 0; int16_t currentPageNextY = 0;
@@ -37,23 +46,53 @@ class ChapterHtmlSlimParser {
uint16_t viewportWidth; uint16_t viewportWidth;
uint16_t viewportHeight; uint16_t viewportHeight;
bool hyphenationEnabled; bool hyphenationEnabled;
const CssParser* cssParser;
bool embeddedStyle;
std::string contentBase;
std::string imageBasePath;
int imageCounter = 0;
void startNewTextBlock(TextBlock::Style style); // Style tracking (replaces depth-based approach)
struct StyleStackEntry {
int depth = 0;
bool hasBold = false, bold = false;
bool hasItalic = false, italic = false;
bool hasUnderline = false, underline = false;
};
std::vector<StyleStackEntry> inlineStyleStack;
CssStyle currentCssStyle;
bool effectiveBold = false;
bool effectiveItalic = false;
bool effectiveUnderline = false;
// Table buffering state
bool inTable = false;
std::unique_ptr<TableData> tableData;
void updateEffectiveInlineStyle();
void startNewTextBlock(const BlockStyle& blockStyle);
void flushPartWordBuffer(); void flushPartWordBuffer();
void makePages(); void makePages();
void processTable();
void addTableRowToPage(std::shared_ptr<PageTableRow> row);
// 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);
static void XMLCALL characterData(void* userData, const XML_Char* s, int len); static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
static void XMLCALL endElement(void* userData, const XML_Char* name); static void XMLCALL endElement(void* userData, const XML_Char* name);
public: public:
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
const float lineCompression, const bool extraParagraphSpacing, const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn, const std::function<void(std::unique_ptr<Page>)>& completePageFn,
const std::function<void(int)>& progressFn = nullptr) const bool embeddedStyle, const std::string& contentBase,
: filepath(filepath), const std::string& imageBasePath, const std::function<void()>& popupFn = nullptr,
const CssParser* cssParser = nullptr)
: epub(epub),
filepath(filepath),
renderer(renderer), renderer(renderer),
fontId(fontId), fontId(fontId),
lineCompression(lineCompression), lineCompression(lineCompression),
@@ -63,7 +102,12 @@ class ChapterHtmlSlimParser {
viewportHeight(viewportHeight), viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled), hyphenationEnabled(hyphenationEnabled),
completePageFn(completePageFn), completePageFn(completePageFn),
progressFn(progressFn) {} popupFn(popupFn),
cssParser(cssParser),
embeddedStyle(embeddedStyle),
contentBase(contentBase),
imageBasePath(imageBasePath) {}
~ChapterHtmlSlimParser() = default; ~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages(); bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line); void addLineToPage(std::shared_ptr<TextBlock> line);

View File

@@ -1,11 +1,11 @@
#include "ContainerParser.h" #include "ContainerParser.h"
#include <HardwareSerial.h> #include <Logging.h>
bool ContainerParser::setup() { bool ContainerParser::setup() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis()); LOG_ERR("CTR", "Couldn't allocate memory for parser");
return false; return false;
} }
@@ -34,7 +34,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
while (remainingInBuffer > 0) { while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024); void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) { if (!buf) {
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis()); LOG_DBG("CTR", "Couldn't allocate buffer");
return 0; return 0;
} }
@@ -42,7 +42,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
memcpy(buf, currentBufferPos, toRead); memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser))); LOG_ERR("CTR", "Parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
return 0; return 0;
} }

View File

@@ -1,20 +1,21 @@
#include "ContentOpfParser.h" #include "ContentOpfParser.h"
#include <FsHelpers.h> #include <FsHelpers.h>
#include <HardwareSerial.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include "../BookMetadataCache.h" #include "../BookMetadataCache.h"
namespace { namespace {
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
constexpr char MEDIA_TYPE_CSS[] = "text/css";
constexpr char itemCacheFile[] = "/.items.bin"; constexpr char itemCacheFile[] = "/.items.bin";
} // namespace } // namespace
bool ContentOpfParser::setup() { bool ContentOpfParser::setup() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis()); LOG_DBG("COF", "Couldn't allocate memory for parser");
return false; return false;
} }
@@ -35,8 +36,8 @@ ContentOpfParser::~ContentOpfParser() {
if (tempItemStore) { if (tempItemStore) {
tempItemStore.close(); tempItemStore.close();
} }
if (SdMan.exists((cachePath + itemCacheFile).c_str())) { if (Storage.exists((cachePath + itemCacheFile).c_str())) {
SdMan.remove((cachePath + itemCacheFile).c_str()); Storage.remove((cachePath + itemCacheFile).c_str());
} }
itemIndex.clear(); itemIndex.clear();
itemIndex.shrink_to_fit(); itemIndex.shrink_to_fit();
@@ -55,7 +56,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
void* const buf = XML_GetBuffer(parser, 1024); void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) { if (!buf) {
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis()); LOG_ERR("COF", "Couldn't allocate memory for buffer");
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
XML_SetCharacterDataHandler(parser, nullptr); XML_SetCharacterDataHandler(parser, nullptr);
@@ -68,7 +69,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
memcpy(buf, currentBufferPos, toRead); memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), LOG_DBG("COF", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser))); XML_ErrorString(XML_GetErrorCode(parser)));
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
@@ -117,20 +118,16 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
self->state = IN_MANIFEST; self->state = IN_MANIFEST;
if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
Serial.printf( LOG_ERR("COF", "Couldn't open temp items file for writing. This is probably going to be a fatal error.");
"[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n",
millis());
} }
return; return;
} }
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) { if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
self->state = IN_SPINE; self->state = IN_SPINE;
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
Serial.printf( LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
millis());
} }
// Sort item index for binary search if we have enough items // Sort item index for binary search if we have enough items
@@ -139,7 +136,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen); return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
}); });
self->useItemIndex = true; self->useItemIndex = true;
Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size()); LOG_DBG("COF", "Using fast index for %zu manifest items", self->itemIndex.size());
} }
return; return;
} }
@@ -147,11 +144,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) { if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
self->state = IN_GUIDE; self->state = IN_GUIDE;
// TODO Remove print // TODO Remove print
Serial.printf("[%lu] [COF] Entering guide state.\n", millis()); LOG_DBG("COF", "Entering guide state.");
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
Serial.printf( LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
millis());
} }
return; return;
} }
@@ -213,17 +208,29 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->tocNcxPath.empty()) { if (self->tocNcxPath.empty()) {
self->tocNcxPath = href; self->tocNcxPath = href;
} else { } else {
Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(), LOG_DBG("COF", "Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s", href.c_str());
href.c_str());
} }
} }
// Collect CSS files
if (mediaType == MEDIA_TYPE_CSS) {
self->cssFiles.push_back(href);
}
// EPUB 3: Check for nav document (properties contains "nav") // EPUB 3: Check for nav document (properties contains "nav")
if (!properties.empty() && self->tocNavPath.empty()) { if (!properties.empty() && self->tocNavPath.empty()) {
// Properties is space-separated, check if "nav" is present as a word // Properties is space-separated, check if "nav" is present as a word
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) { if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
self->tocNavPath = href; self->tocNavPath = href;
Serial.printf("[%lu] [COF] Found EPUB 3 nav document: %s\n", millis(), href.c_str()); LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str());
}
}
// EPUB 3: Check for cover image (properties contains "cover-image")
if (!properties.empty() && self->coverItemHref.empty()) {
if (properties == "cover-image" || properties.find("cover-image ") == 0 ||
properties.find(" cover-image") != std::string::npos) {
self->coverItemHref = href;
} }
} }
return; return;
@@ -296,7 +303,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (type == "text" || type == "start") { if (type == "text" || type == "start") {
continue; continue;
} else { } else {
Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str()); LOG_DBG("COF", "Skipping non-text reference in guide: %s", type.c_str());
break; break;
} }
} else if (strcmp(atts[i], "href") == 0) { } else if (strcmp(atts[i], "href") == 0) {
@@ -304,7 +311,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
} }
} }
if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) { if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) {
Serial.printf("[%lu] [COF] Found %s reference in guide: %s.\n", millis(), type.c_str(), textHref.c_str()); LOG_DBG("COF", "Found %s reference in guide: %s.", type.c_str(), textHref.c_str());
self->textReferenceHref = textHref; self->textReferenceHref = textHref;
} }
return; return;
@@ -320,6 +327,9 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s,
} }
if (self->state == IN_BOOK_AUTHOR) { if (self->state == IN_BOOK_AUTHOR) {
if (!self->author.empty()) {
self->author.append(", "); // Add separator for multiple authors
}
self->author.append(s, len); self->author.append(s, len);
return; return;
} }

View File

@@ -64,6 +64,7 @@ class ContentOpfParser final : public Print {
std::string tocNavPath; // EPUB 3 nav document path std::string tocNavPath; // EPUB 3 nav document path
std::string coverItemHref; std::string coverItemHref;
std::string textReferenceHref; std::string textReferenceHref;
std::vector<std::string> cssFiles; // CSS stylesheet paths
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize, explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
BookMetadataCache* cache) BookMetadataCache* cache)

View File

@@ -1,14 +1,14 @@
#include "TocNavParser.h" #include "TocNavParser.h"
#include <FsHelpers.h> #include <FsHelpers.h>
#include <HardwareSerial.h> #include <Logging.h>
#include "../BookMetadataCache.h" #include "../BookMetadataCache.h"
bool TocNavParser::setup() { bool TocNavParser::setup() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
Serial.printf("[%lu] [NAV] Couldn't allocate memory for parser\n", millis()); LOG_DBG("NAV", "Couldn't allocate memory for parser");
return false; return false;
} }
@@ -39,7 +39,7 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
while (remainingInBuffer > 0) { while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024); void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) { if (!buf) {
Serial.printf("[%lu] [NAV] Couldn't allocate memory for buffer\n", millis()); LOG_DBG("NAV", "Couldn't allocate memory for buffer");
XML_StopParser(parser, XML_FALSE); XML_StopParser(parser, XML_FALSE);
XML_SetElementHandler(parser, nullptr, nullptr); XML_SetElementHandler(parser, nullptr, nullptr);
XML_SetCharacterDataHandler(parser, nullptr); XML_SetCharacterDataHandler(parser, nullptr);
@@ -52,7 +52,7 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
memcpy(buf, currentBufferPos, toRead); memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [NAV] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), LOG_DBG("NAV", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser))); XML_ErrorString(XML_GetErrorCode(parser)));
XML_StopParser(parser, XML_FALSE); XML_StopParser(parser, XML_FALSE);
XML_SetElementHandler(parser, nullptr, nullptr); XML_SetElementHandler(parser, nullptr, nullptr);
@@ -88,7 +88,7 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co
for (int i = 0; atts[i]; i += 2) { for (int i = 0; atts[i]; i += 2) {
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) { if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
self->state = IN_NAV_TOC; self->state = IN_NAV_TOC;
Serial.printf("[%lu] [NAV] Found nav toc element\n", millis()); LOG_DBG("NAV", "Found nav toc element");
return; return;
} }
} }
@@ -179,7 +179,7 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) { if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
self->state = IN_BODY; self->state = IN_BODY;
Serial.printf("[%lu] [NAV] Finished parsing nav toc\n", millis()); LOG_DBG("NAV", "Finished parsing nav toc");
return; return;
} }
} }

View File

@@ -1,14 +1,14 @@
#include "TocNcxParser.h" #include "TocNcxParser.h"
#include <FsHelpers.h> #include <FsHelpers.h>
#include <HardwareSerial.h> #include <Logging.h>
#include "../BookMetadataCache.h" #include "../BookMetadataCache.h"
bool TocNcxParser::setup() { bool TocNcxParser::setup() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis()); LOG_DBG("TOC", "Couldn't allocate memory for parser");
return false; return false;
} }
@@ -39,7 +39,7 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
while (remainingInBuffer > 0) { while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024); void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) { if (!buf) {
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis()); LOG_DBG("TOC", "Couldn't allocate memory for buffer");
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
XML_SetCharacterDataHandler(parser, nullptr); XML_SetCharacterDataHandler(parser, nullptr);
@@ -52,7 +52,7 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
memcpy(buf, currentBufferPos, toRead); memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), LOG_DBG("TOC", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser))); XML_ErrorString(XML_GetErrorCode(parser)));
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <SdFat.h> #include <HalStorage.h>
#include <cstdint> #include <cstdint>

View File

@@ -104,3 +104,20 @@ uint8_t quantize1bit(int gray, int x, int y) {
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192 const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
return (gray >= adjustedThreshold) ? 1 : 0; return (gray >= adjustedThreshold) ? 1 : 0;
} }
// Noise dithering for gradient fills - always uses hash-based noise regardless of global dithering config.
// Produces smooth-looking gradients on the 4-level e-ink display.
uint8_t quantizeNoiseDither(int gray, int x, int y) {
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
hash = (hash ^ (hash >> 13)) * 1274126177u;
const int threshold = static_cast<int>(hash >> 24);
const int scaled = gray * 3;
if (scaled < 255) {
return (scaled + threshold >= 255) ? 1 : 0;
} else if (scaled < 510) {
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
} else {
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
}
}

View File

@@ -7,6 +7,7 @@ uint8_t quantize(int gray, int x, int y);
uint8_t quantizeSimple(int gray); uint8_t quantizeSimple(int gray);
uint8_t quantize1bit(int gray, int x, int y); uint8_t quantize1bit(int gray, int x, int y);
int adjustPixel(int gray); int adjustPixel(int gray);
uint8_t quantizeNoiseDither(int gray, int x, int y);
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails // 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
// Error distribution pattern (same as 2-bit but quantizes to 2 levels): // Error distribution pattern (same as 2-bit but quantizes to 2 levels):

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,10 @@
#include "Bitmap.h" #include "Bitmap.h"
// Color representation: uint8_t mapped to 4x4 Bayer matrix dithering levels
// 0 = transparent, 1-16 = gray levels (white to black)
enum Color : uint8_t { Clear = 0x00, White = 0x01, LightGray = 0x05, DarkGray = 0x0A, Black = 0x10 };
class GfxRenderer { class GfxRenderer {
public: public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
@@ -28,15 +32,21 @@ class GfxRenderer {
HalDisplay& display; HalDisplay& display;
RenderMode renderMode; RenderMode renderMode;
Orientation orientation; Orientation orientation;
bool fadingFix;
uint8_t* frameBuffer = nullptr;
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap; std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
EpdFontFamily::Style style) const; EpdFontFamily::Style style) const;
void freeBwBufferChunks(); void freeBwBufferChunks();
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; template <Color color>
void drawPixelDither(int x, int y) const;
template <Color color>
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
public: public:
explicit GfxRenderer(HalDisplay& halDisplay) : display(halDisplay), renderMode(BW), orientation(Portrait) {} explicit GfxRenderer(HalDisplay& halDisplay)
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
~GfxRenderer() { freeBwBufferChunks(); } ~GfxRenderer() { freeBwBufferChunks(); }
static constexpr int VIEWABLE_MARGIN_TOP = 9; static constexpr int VIEWABLE_MARGIN_TOP = 9;
@@ -45,27 +55,45 @@ class GfxRenderer {
static constexpr int VIEWABLE_MARGIN_LEFT = 3; static constexpr int VIEWABLE_MARGIN_LEFT = 3;
// Setup // Setup
void begin(); // must be called right after display.begin()
void insertFont(int fontId, EpdFontFamily font); void insertFont(int fontId, EpdFontFamily font);
// Orientation control (affects logical width/height and coordinate transforms) // Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation = o; } void setOrientation(const Orientation o) { orientation = o; }
Orientation getOrientation() const { return orientation; } Orientation getOrientation() const { return orientation; }
// Fading fix control
void setFadingFix(const bool enabled) { fadingFix = enabled; }
// Screen ops // Screen ops
int getScreenWidth() const; int getScreenWidth() const;
int getScreenHeight() const; int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region // EXPERIMENTAL: Windowed update - display only a rectangular region
void displayWindow(int x, int y, int width, int height) const; void displayWindow(int x, int y, int width, int height,
HalDisplay::RefreshMode mode = HalDisplay::FAST_REFRESH) const;
void invertScreen() const; void invertScreen() const;
void clearScreen(uint8_t color = 0xFF) const; void clearScreen(uint8_t color = 0xFF) const;
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
// Drawing // Drawing
void drawPixel(int x, int y, bool state = true) const; void drawPixel(int x, int y, bool state = true) const;
void drawPixelGray(int x, int y, uint8_t val2bit) const;
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
void drawRect(int x, int y, int width, int height, bool state = true) const; void drawRect(int x, int y, int width, int height, bool state = true) const;
void drawRect(int x, int y, int width, int height, int lineWidth, bool state) const;
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool roundTopLeft,
bool roundTopRight, bool roundBottomLeft, bool roundBottomRight, bool state) const;
void fillRect(int x, int y, int width, int height, bool state = true) const; void fillRect(int x, int y, int width, int height, bool state = true) const;
void fillRectDither(int x, int y, int width, int height, Color color) const;
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, Color color) const;
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
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 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;
@@ -78,24 +106,22 @@ class GfxRenderer {
void drawText(int fontId, int x, int y, const char* text, bool black = true, void drawText(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getSpaceWidth(int fontId) const; int getSpaceWidth(int fontId) const;
int getTextAdvanceX(int fontId, const char* text) const;
int getFontAscenderSize(int fontId) const; int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const; int getLineHeight(int fontId) const;
std::string truncatedText(int fontId, const char* text, int maxWidth, std::string truncatedText(int fontId, const char* text, int maxWidth,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
// UI Components // Helpers for drawing rotated text (for side buttons)
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
private:
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true, void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
void drawTextRotated90CCW(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getTextHeight(int fontId) const; int getTextHeight(int fontId) const;
public:
// 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; }
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer() const;
@@ -106,6 +132,4 @@ class GfxRenderer {
// Low level functions // Low level functions
uint8_t* getFrameBuffer() const; uint8_t* getFrameBuffer() const;
static size_t getBufferSize(); static size_t getBufferSize();
void grayscaleRevert() const;
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
}; };

96
lib/I18n/I18n.cpp Normal file
View File

@@ -0,0 +1,96 @@
#include "I18n.h"
#include <HalStorage.h>
#include <HardwareSerial.h>
#include <Serialization.h>
#include "I18nStrings.h"
using namespace i18n_strings;
// Settings file path
static constexpr const char* SETTINGS_FILE = "/.crosspoint/language.bin";
static constexpr uint8_t SETTINGS_VERSION = 1;
I18n& I18n::getInstance() {
static I18n instance;
return instance;
}
const char* I18n::get(StrId id) const {
const auto index = static_cast<size_t>(id);
if (index >= static_cast<size_t>(StrId::_COUNT)) {
return "???";
}
// Use generated helper function - no hardcoded switch needed!
const char* const* strings = getStringArray(_language);
return strings[index];
}
void I18n::setLanguage(Language lang) {
if (lang >= Language::_COUNT) {
return;
}
_language = lang;
saveSettings();
}
const char* I18n::getLanguageName(Language lang) const {
const auto index = static_cast<size_t>(lang);
if (index >= static_cast<size_t>(Language::_COUNT)) {
return "???";
}
return LANGUAGE_NAMES[index];
}
void I18n::saveSettings() {
Storage.mkdir("/.crosspoint");
FsFile file;
if (!Storage.openFileForWrite("I18N", SETTINGS_FILE, file)) {
Serial.printf("[I18N] Failed to save settings\n");
return;
}
serialization::writePod(file, SETTINGS_VERSION);
serialization::writePod(file, static_cast<uint8_t>(_language));
file.close();
Serial.printf("[I18N] Settings saved: language=%d\n", static_cast<int>(_language));
}
void I18n::loadSettings() {
FsFile file;
if (!Storage.openFileForRead("I18N", SETTINGS_FILE, file)) {
Serial.printf("[I18N] No settings file, using default (English)\n");
return;
}
uint8_t version;
serialization::readPod(file, version);
if (version != SETTINGS_VERSION) {
Serial.printf("[I18N] Settings version mismatch\n");
file.close();
return;
}
uint8_t lang;
serialization::readPod(file, lang);
if (lang < static_cast<size_t>(Language::_COUNT)) {
_language = static_cast<Language>(lang);
Serial.printf("[I18N] Loaded language: %d\n", static_cast<int>(_language));
}
file.close();
}
// Generate character set for a specific language
const char* I18n::getCharacterSet(Language lang) {
const auto langIndex = static_cast<size_t>(lang);
if (langIndex >= static_cast<size_t>(Language::_COUNT)) {
lang = Language::ENGLISH; // Fallback to first language
}
return CHARACTER_SETS[static_cast<size_t>(lang)];
}

42
lib/I18n/I18n.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
#include "I18nKeys.h"
/**
* Internationalization (i18n) system for CrossPoint Reader
*/
class I18n {
public:
static I18n& getInstance();
// Disable copy
I18n(const I18n&) = delete;
I18n& operator=(const I18n&) = delete;
// Get localized string by ID
const char* get(StrId id) const;
const char* operator[](StrId id) const { return get(id); }
Language getLanguage() const { return _language; }
void setLanguage(Language lang);
const char* getLanguageName(Language lang) const;
void saveSettings();
void loadSettings();
// Get all unique characters used in a specific language
// Returns a sorted string of unique characters
static const char* getCharacterSet(Language lang);
private:
I18n() : _language(Language::ENGLISH) {}
Language _language;
};
// Convenience macros
#define tr(id) I18n::getInstance().get(StrId::id)
#define I18N I18n::getInstance()

422
lib/I18n/I18nKeys.h Normal file
View File

@@ -0,0 +1,422 @@
#pragma once
#include <cstdint>
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
// Forward declaration for string arrays
namespace i18n_strings {
extern const char* const STRINGS_EN[];
extern const char* const STRINGS_ES[];
extern const char* const STRINGS_FR[];
extern const char* const STRINGS_DE[];
extern const char* const STRINGS_CZ[];
extern const char* const STRINGS_PO[];
extern const char* const STRINGS_RU[];
extern const char* const STRINGS_SV[];
} // namespace i18n_strings
// Language enum
enum class Language : uint8_t {
ENGLISH = 0,
SPANISH = 1,
FRENCH = 2,
GERMAN = 3,
CZECH = 4,
PORTUGUESE = 5,
RUSSIAN = 6,
SWEDISH = 7,
_COUNT
};
// Language display names (defined in I18nStrings.cpp)
extern const char* const LANGUAGE_NAMES[];
// Character sets for each language (defined in I18nStrings.cpp)
extern const char* const CHARACTER_SETS[];
// String IDs
enum class StrId : uint16_t {
STR_CROSSPOINT,
STR_BOOTING,
STR_SLEEPING,
STR_ENTERING_SLEEP,
STR_BROWSE_FILES,
STR_FILE_TRANSFER,
STR_SETTINGS_TITLE,
STR_CALIBRE_LIBRARY,
STR_CONTINUE_READING,
STR_NO_OPEN_BOOK,
STR_START_READING,
STR_BOOKS,
STR_NO_BOOKS_FOUND,
STR_SELECT_CHAPTER,
STR_NO_CHAPTERS,
STR_END_OF_BOOK,
STR_EMPTY_CHAPTER,
STR_INDEXING,
STR_MEMORY_ERROR,
STR_PAGE_LOAD_ERROR,
STR_EMPTY_FILE,
STR_OUT_OF_BOUNDS,
STR_LOADING,
STR_LOAD_XTC_FAILED,
STR_LOAD_TXT_FAILED,
STR_LOAD_EPUB_FAILED,
STR_SD_CARD_ERROR,
STR_WIFI_NETWORKS,
STR_NO_NETWORKS,
STR_NETWORKS_FOUND,
STR_SCANNING,
STR_CONNECTING,
STR_CONNECTED,
STR_CONNECTION_FAILED,
STR_CONNECTION_TIMEOUT,
STR_FORGET_NETWORK,
STR_SAVE_PASSWORD,
STR_REMOVE_PASSWORD,
STR_PRESS_OK_SCAN,
STR_PRESS_ANY_CONTINUE,
STR_SELECT_HINT,
STR_HOW_CONNECT,
STR_JOIN_NETWORK,
STR_CREATE_HOTSPOT,
STR_JOIN_DESC,
STR_HOTSPOT_DESC,
STR_STARTING_HOTSPOT,
STR_HOTSPOT_MODE,
STR_CONNECT_WIFI_HINT,
STR_OPEN_URL_HINT,
STR_OR_HTTP_PREFIX,
STR_SCAN_QR_HINT,
STR_CALIBRE_WIRELESS,
STR_CALIBRE_WEB_URL,
STR_CONNECT_WIRELESS,
STR_NETWORK_LEGEND,
STR_MAC_ADDRESS,
STR_CHECKING_WIFI,
STR_ENTER_WIFI_PASSWORD,
STR_ENTER_TEXT,
STR_TO_PREFIX,
STR_CALIBRE_DISCOVERING,
STR_CALIBRE_CONNECTING_TO,
STR_CALIBRE_CONNECTED_TO,
STR_CALIBRE_WAITING_COMMANDS,
STR_CONNECTION_FAILED_RETRYING,
STR_CALIBRE_DISCONNECTED,
STR_CALIBRE_WAITING_TRANSFER,
STR_CALIBRE_TRANSFER_HINT,
STR_CALIBRE_RECEIVING,
STR_CALIBRE_RECEIVED,
STR_CALIBRE_WAITING_MORE,
STR_CALIBRE_FAILED_CREATE_FILE,
STR_CALIBRE_PASSWORD_REQUIRED,
STR_CALIBRE_TRANSFER_INTERRUPTED,
STR_CALIBRE_INSTRUCTION_1,
STR_CALIBRE_INSTRUCTION_2,
STR_CALIBRE_INSTRUCTION_3,
STR_CALIBRE_INSTRUCTION_4,
STR_CAT_DISPLAY,
STR_CAT_READER,
STR_CAT_CONTROLS,
STR_CAT_SYSTEM,
STR_CAT_CLOCK,
STR_SLEEP_SCREEN,
STR_SLEEP_COVER_MODE,
STR_STATUS_BAR,
STR_HIDE_BATTERY,
STR_EXTRA_SPACING,
STR_TEXT_AA,
STR_SHORT_PWR_BTN,
STR_ORIENTATION,
STR_FRONT_BTN_LAYOUT,
STR_SIDE_BTN_LAYOUT,
STR_LONG_PRESS_SKIP,
STR_FONT_FAMILY,
STR_EXT_READER_FONT,
STR_EXT_CHINESE_FONT,
STR_EXT_UI_FONT,
STR_FONT_SIZE,
STR_LINE_SPACING,
STR_ASCII_LETTER_SPACING,
STR_ASCII_DIGIT_SPACING,
STR_CJK_SPACING,
STR_COLOR_MODE,
STR_SCREEN_MARGIN,
STR_PARA_ALIGNMENT,
STR_HYPHENATION,
STR_TIME_TO_SLEEP,
STR_REFRESH_FREQ,
STR_CALIBRE_SETTINGS,
STR_KOREADER_SYNC,
STR_CHECK_UPDATES,
STR_LANGUAGE,
STR_SELECT_WALLPAPER,
STR_CLEAR_READING_CACHE,
STR_CALIBRE,
STR_USERNAME,
STR_PASSWORD,
STR_SYNC_SERVER_URL,
STR_DOCUMENT_MATCHING,
STR_AUTHENTICATE,
STR_KOREADER_USERNAME,
STR_KOREADER_PASSWORD,
STR_FILENAME,
STR_BINARY,
STR_SET_CREDENTIALS_FIRST,
STR_WIFI_CONN_FAILED,
STR_AUTHENTICATING,
STR_AUTH_SUCCESS,
STR_KOREADER_AUTH,
STR_SYNC_READY,
STR_AUTH_FAILED,
STR_DONE,
STR_CLEAR_CACHE_WARNING_1,
STR_CLEAR_CACHE_WARNING_2,
STR_CLEAR_CACHE_WARNING_3,
STR_CLEAR_CACHE_WARNING_4,
STR_CLEARING_CACHE,
STR_CACHE_CLEARED,
STR_ITEMS_REMOVED,
STR_FAILED_LOWER,
STR_CLEAR_CACHE_FAILED,
STR_CHECK_SERIAL_OUTPUT,
STR_DARK,
STR_LIGHT,
STR_CUSTOM,
STR_COVER,
STR_NONE_OPT,
STR_FIT,
STR_CROP,
STR_NO_PROGRESS,
STR_FULL_OPT,
STR_NEVER,
STR_IN_READER,
STR_ALWAYS,
STR_IGNORE,
STR_SLEEP,
STR_PAGE_TURN,
STR_PORTRAIT,
STR_LANDSCAPE_CW,
STR_INVERTED,
STR_LANDSCAPE_CCW,
STR_FRONT_LAYOUT_BCLR,
STR_FRONT_LAYOUT_LRBC,
STR_FRONT_LAYOUT_LBCR,
STR_PREV_NEXT,
STR_NEXT_PREV,
STR_BOOKERLY,
STR_NOTO_SANS,
STR_OPEN_DYSLEXIC,
STR_SMALL,
STR_MEDIUM,
STR_LARGE,
STR_X_LARGE,
STR_TIGHT,
STR_NORMAL,
STR_WIDE,
STR_JUSTIFY,
STR_ALIGN_LEFT,
STR_CENTER,
STR_ALIGN_RIGHT,
STR_MIN_1,
STR_MIN_5,
STR_MIN_10,
STR_MIN_15,
STR_MIN_30,
STR_PAGES_1,
STR_PAGES_5,
STR_PAGES_10,
STR_PAGES_15,
STR_PAGES_30,
STR_UPDATE,
STR_CHECKING_UPDATE,
STR_NEW_UPDATE,
STR_CURRENT_VERSION,
STR_NEW_VERSION,
STR_UPDATING,
STR_NO_UPDATE,
STR_UPDATE_FAILED,
STR_UPDATE_COMPLETE,
STR_POWER_ON_HINT,
STR_EXTERNAL_FONT,
STR_BUILTIN_DISABLED,
STR_NO_ENTRIES,
STR_DOWNLOADING,
STR_DOWNLOAD_FAILED,
STR_ERROR_MSG,
STR_UNNAMED,
STR_NO_SERVER_URL,
STR_FETCH_FEED_FAILED,
STR_PARSE_FEED_FAILED,
STR_NETWORK_PREFIX,
STR_IP_ADDRESS_PREFIX,
STR_SCAN_QR_WIFI_HINT,
STR_ERROR_GENERAL_FAILURE,
STR_ERROR_NETWORK_NOT_FOUND,
STR_ERROR_CONNECTION_TIMEOUT,
STR_SD_CARD,
STR_BACK,
STR_EXIT,
STR_HOME,
STR_SAVE,
STR_SELECT,
STR_TOGGLE,
STR_CONFIRM,
STR_CANCEL,
STR_CONNECT,
STR_OPEN,
STR_DOWNLOAD,
STR_RETRY,
STR_YES,
STR_NO,
STR_STATE_ON,
STR_STATE_OFF,
STR_SET,
STR_NOT_SET,
STR_DIR_LEFT,
STR_DIR_RIGHT,
STR_DIR_UP,
STR_DIR_DOWN,
STR_CAPS_ON,
STR_CAPS_OFF,
STR_OK_BUTTON,
STR_ON_MARKER,
STR_SLEEP_COVER_FILTER,
STR_FILTER_CONTRAST,
STR_STATUS_BAR_FULL_PERCENT,
STR_STATUS_BAR_FULL_BOOK,
STR_STATUS_BAR_BOOK_ONLY,
STR_STATUS_BAR_FULL_CHAPTER,
STR_UI_THEME,
STR_THEME_CLASSIC,
STR_THEME_LYRA,
STR_SUNLIGHT_FADING_FIX,
STR_REMAP_FRONT_BUTTONS,
STR_OPDS_BROWSER,
STR_COVER_CUSTOM,
STR_RECENTS,
STR_MENU_RECENT_BOOKS,
STR_NO_RECENT_BOOKS,
STR_CALIBRE_DESC,
STR_FORGET_AND_REMOVE,
STR_FORGET_BUTTON,
STR_CALIBRE_STARTING,
STR_CALIBRE_SETUP,
STR_CALIBRE_STATUS,
STR_CLEAR_BUTTON,
STR_DEFAULT_VALUE,
STR_REMAP_PROMPT,
STR_UNASSIGNED,
STR_ALREADY_ASSIGNED,
STR_REMAP_RESET_HINT,
STR_REMAP_CANCEL_HINT,
STR_HW_BACK_LABEL,
STR_HW_CONFIRM_LABEL,
STR_HW_LEFT_LABEL,
STR_HW_RIGHT_LABEL,
STR_GO_TO_PERCENT,
STR_GO_HOME_BUTTON,
STR_SYNC_PROGRESS,
STR_DELETE_CACHE,
STR_CHAPTER_PREFIX,
STR_PAGES_SEPARATOR,
STR_BOOK_PREFIX,
STR_KBD_SHIFT,
STR_KBD_SHIFT_CAPS,
STR_KBD_LOCK,
STR_CALIBRE_URL_HINT,
STR_PERCENT_STEP_HINT,
STR_SYNCING_TIME,
STR_CALC_HASH,
STR_HASH_FAILED,
STR_FETCH_PROGRESS,
STR_UPLOAD_PROGRESS,
STR_NO_CREDENTIALS_MSG,
STR_KOREADER_SETUP_HINT,
STR_PROGRESS_FOUND,
STR_REMOTE_LABEL,
STR_LOCAL_LABEL,
STR_PAGE_OVERALL_FORMAT,
STR_PAGE_TOTAL_OVERALL_FORMAT,
STR_DEVICE_FROM_FORMAT,
STR_APPLY_REMOTE,
STR_UPLOAD_LOCAL,
STR_NO_REMOTE_MSG,
STR_UPLOAD_PROMPT,
STR_UPLOAD_SUCCESS,
STR_SYNC_FAILED_MSG,
STR_SECTION_PREFIX,
STR_UPLOAD,
STR_BOOK_S_STYLE,
STR_EMBEDDED_STYLE,
STR_OPDS_SERVER_URL,
STR_LETTERBOX_FILL,
STR_DITHERED,
STR_SOLID,
STR_ADD_BOOKMARK,
STR_REMOVE_BOOKMARK,
STR_LOOKUP_WORD,
STR_LOOKUP_HISTORY,
STR_GO_TO_BOOKMARK,
STR_CLOSE_BOOK,
STR_DELETE_DICT_CACHE,
STR_DEFAULT_OPTION,
STR_BOOKMARK_ADDED,
STR_BOOKMARK_REMOVED,
STR_DICT_CACHE_DELETED,
STR_NO_CACHE_TO_DELETE,
STR_TABLE_OF_CONTENTS,
STR_TOGGLE_ORIENTATION,
STR_TOGGLE_FONT_SIZE,
STR_OVERRIDE_LETTERBOX_FILL,
STR_PREFERRED_PORTRAIT,
STR_PREFERRED_LANDSCAPE,
STR_CHOOSE_SOMETHING,
STR_CLOCK,
STR_CLOCK_AMPM,
STR_CLOCK_24H,
STR_SET_TIME,
STR_CLOCK_SIZE,
STR_CLOCK_SIZE_SMALL,
STR_CLOCK_SIZE_MEDIUM,
STR_CLOCK_SIZE_LARGE,
STR_TIMEZONE,
STR_TZ_UTC,
STR_TZ_EASTERN,
STR_TZ_CENTRAL,
STR_TZ_MOUNTAIN,
STR_TZ_PACIFIC,
STR_TZ_ALASKA,
STR_TZ_HAWAII,
STR_TZ_CUSTOM,
STR_SET_UTC_OFFSET,
// Sentinel - must be last
_COUNT
};
// Helper function to get string array for a language
inline const char* const* getStringArray(Language lang) {
switch (lang) {
case Language::ENGLISH:
return i18n_strings::STRINGS_EN;
case Language::SPANISH:
return i18n_strings::STRINGS_ES;
case Language::FRENCH:
return i18n_strings::STRINGS_FR;
case Language::GERMAN:
return i18n_strings::STRINGS_DE;
case Language::CZECH:
return i18n_strings::STRINGS_CZ;
case Language::PORTUGUESE:
return i18n_strings::STRINGS_PO;
case Language::RUSSIAN:
return i18n_strings::STRINGS_RU;
case Language::SWEDISH:
return i18n_strings::STRINGS_SV;
default:
return i18n_strings::STRINGS_EN;
}
}
// Helper function to get language count
constexpr uint8_t getLanguageCount() { return static_cast<uint8_t>(Language::_COUNT); }

19
lib/I18n/I18nStrings.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <string>
#include "I18nKeys.h"
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
namespace i18n_strings {
extern const char* const STRINGS_EN[];
extern const char* const STRINGS_ES[];
extern const char* const STRINGS_FR[];
extern const char* const STRINGS_DE[];
extern const char* const STRINGS_CZ[];
extern const char* const STRINGS_PO[];
extern const char* const STRINGS_RU[];
extern const char* const STRINGS_SV[];
} // namespace i18n_strings

View File

@@ -0,0 +1,337 @@
_language_name: "Čeština"
_language_code: "CZECH"
_order: "4"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPUŠTĚNÍ"
STR_SLEEPING: "SPÁNEK"
STR_ENTERING_SLEEP: "Vstup do režimu spánku..."
STR_BROWSE_FILES: "Procházet soubory"
STR_FILE_TRANSFER: "Přenos souborů"
STR_SETTINGS_TITLE: "Nastavení"
STR_CALIBRE_LIBRARY: "Knihovna Calibre"
STR_CONTINUE_READING: "Pokračovat ve čtení"
STR_NO_OPEN_BOOK: "Žádná otevřená kniha"
STR_START_READING: "Začněte číst níže"
STR_BOOKS: "Knihy"
STR_NO_BOOKS_FOUND: "Žádné knihy nenalezeny"
STR_SELECT_CHAPTER: "Vybrat kapitolu"
STR_NO_CHAPTERS: "Žádné kapitoly"
STR_END_OF_BOOK: "Konec knihy"
STR_EMPTY_CHAPTER: "Prázdná kapitola"
STR_INDEXING: "Indexování..."
STR_MEMORY_ERROR: "Chyba paměti"
STR_PAGE_LOAD_ERROR: "Chyba načítání stránky"
STR_EMPTY_FILE: "Prázdný soubor"
STR_OUT_OF_BOUNDS: "Mimo hranice"
STR_LOADING: "Načítání..."
STR_LOAD_XTC_FAILED: "Nepodařilo se načíst XTC"
STR_LOAD_TXT_FAILED: "Nepodařilo se načíst TXT"
STR_LOAD_EPUB_FAILED: "Nepodařilo se načíst EPUB"
STR_SD_CARD_ERROR: "Chyba SD karty"
STR_WIFI_NETWORKS: "Wi-Fi sítě"
STR_NO_NETWORKS: "Žádné sítě nenalezeny"
STR_NETWORKS_FOUND: "Nalezeno %zu sítí"
STR_SCANNING: "Skenování..."
STR_CONNECTING: "Připojování..."
STR_CONNECTED: "Připojeno!"
STR_CONNECTION_FAILED: "Připojení se nezdařilo"
STR_CONNECTION_TIMEOUT: "Časový limit připojení"
STR_FORGET_NETWORK: "Zapomenout síť?"
STR_SAVE_PASSWORD: "Uložit heslo pro příště?"
STR_REMOVE_PASSWORD: "Odstranit uložené heslo?"
STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování"
STR_PRESS_ANY_CONTINUE: "Pokračujte stiskem libovolné klávesy"
STR_SELECT_HINT: "VLEVO/VPRAVO: Vybrat | OK: Potvrdit"
STR_HOW_CONNECT: "Jak se chcete připojit?"
STR_JOIN_NETWORK: "Připojit se k síti"
STR_CREATE_HOTSPOT: "Vytvořit hotspot"
STR_JOIN_DESC: "Připojit se k existující síti WiFi"
STR_HOTSPOT_DESC: "Vytvořit síť WiFi, ke které se mohou připojit ostatní"
STR_STARTING_HOTSPOT: "Spouštění hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Připojte své zařízení k této síti WiFi"
STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
STR_OR_HTTP_PREFIX: "nebo http://"
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "URL webu Calibre"
STR_CONNECT_WIRELESS: "Připojit jako bezdrátové zařízení"
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola WiFi..."
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi"
STR_ENTER_TEXT: "Zadejte text"
STR_TO_PREFIX: "pro"
STR_CALIBRE_DISCOVERING: "Prozkoumávání Calibre..."
STR_CALIBRE_CONNECTING_TO: "Připojování k"
STR_CALIBRE_CONNECTED_TO: "Připojeno k"
STR_CALIBRE_WAITING_COMMANDS: "Čekám na příkazy…"
STR_CONNECTION_FAILED_RETRYING: "(Připojení se nezdařilo, opakování pokusu)"
STR_CALIBRE_DISCONNECTED: "Calibre odpojeno"
STR_CALIBRE_WAITING_TRANSFER: "Čekání na přenos..."
STR_CALIBRE_TRANSFER_HINT: "Nezdaří-li se přenos, povolte\\n„Ignorovat volné místo“ v Calibre\\nnastavení pluginu SmartDevice."
STR_CALIBRE_RECEIVING: "Příjem:"
STR_CALIBRE_RECEIVED: "Přijato:"
STR_CALIBRE_WAITING_MORE: "Čekání na další..."
STR_CALIBRE_FAILED_CREATE_FILE: "Nepodařilo se vytvořit soubor"
STR_CALIBRE_PASSWORD_REQUIRED: "Vyžadováno heslo"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Přenos přerušen"
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi"
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_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čtečka"
STR_CAT_CONTROLS: "Ovládací prvky"
STR_CAT_SYSTEM: "Systém"
STR_CAT_CLOCK: "Hodiny"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
STR_STATUS_BAR: "Stavový řádek"
STR_HIDE_BATTERY: "Skrýt baterii %"
STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
STR_TEXT_AA: "Vyhlazování textu"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení"
STR_FRONT_BTN_LAYOUT: "Rozvržení předních tlačítek"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
STR_FONT_FAMILY: "Rodina písem čtečky"
STR_EXT_READER_FONT: "Písmo externí čtečky"
STR_EXT_CHINESE_FONT: "Písmo čtečky"
STR_EXT_UI_FONT: "Písmo rozhraní"
STR_FONT_SIZE: "Velikost písma rozhraní"
STR_LINE_SPACING: "Řádkování čtečky"
STR_ASCII_LETTER_SPACING: "Mezery písmen ASCII"
STR_ASCII_DIGIT_SPACING: "Mezery číslic ASCII"
STR_CJK_SPACING: "Mezery CJK"
STR_COLOR_MODE: "Režim barev"
STR_SCREEN_MARGIN: "Okraj obrazovky čtečky"
STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky"
STR_HYPHENATION: "Dělení slov"
STR_TIME_TO_SLEEP: "Čas do uspání"
STR_REFRESH_FREQ: "Frekvence obnovení"
STR_CALIBRE_SETTINGS: "Nastavení Calibre"
STR_KOREADER_SYNC: "KOReaderu Sync"
STR_CHECK_UPDATES: "Zkontrolovat aktualizace"
STR_LANGUAGE: "Jazyk"
STR_SELECT_WALLPAPER: "Vybrat tapetu"
STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Uživatelské jméno"
STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synch. serveru"
STR_DOCUMENT_MATCHING: "Párování dokumentů"
STR_AUTHENTICATE: "Ověření"
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
STR_FILENAME: "Název souboru"
STR_BINARY: "Binární"
STR_SET_CREDENTIALS_FIRST: "Nastavte přihlašovací údaje"
STR_WIFI_CONN_FAILED: "Připojení k Wi-Fi selhalo"
STR_AUTHENTICATING: "Ověřování..."
STR_AUTH_SUCCESS: "Úspěšné ověření!"
STR_KOREADER_AUTH: "Ověření KOReaderu"
STR_SYNC_READY: "Synchronizace KOReaderu je připravena k použití"
STR_AUTH_FAILED: "Ověření selhalo"
STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_1: "Tímto vymažete všechna data knih v mezipaměti."
STR_CLEAR_CACHE_WARNING_2: "Veškerý průběh čtení bude ztracen!"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude nutné znovu indexovat"
STR_CLEAR_CACHE_WARNING_4: "při opětovném otevření."
STR_CLEARING_CACHE: "Mazání mezipaměti..."
STR_CACHE_CLEARED: "Mezipaměť vymazána"
STR_ITEMS_REMOVED: "položky odstraněny"
STR_FAILED_LOWER: "selhalo"
STR_CLEAR_CACHE_FAILED: "Vymazání mezipaměti se nezdařilo"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti naleznete v sériovém výstupu"
STR_DARK: "Tmavý"
STR_LIGHT: "Světlý"
STR_CUSTOM: "Vlastní"
STR_COVER: "Obálka"
STR_NONE_OPT: "Žádný"
STR_FIT: "Přizpůsobit"
STR_CROP: "Oříznout"
STR_NO_PROGRESS: "Žádný postup"
STR_FULL_OPT: "Plná"
STR_NEVER: "Nikdy"
STR_IN_READER: "Ve čtečce"
STR_ALWAYS: "Vždy"
STR_IGNORE: "Ignorovat"
STR_SLEEP: "Spánek"
STR_PAGE_TURN: "Otáčení stránek"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_FRONT_LAYOUT_BCLR: "Zpět, Potvrdit, Vlevo, Vpravo"
STR_FRONT_LAYOUT_LRBC: "Vlevo, Vpravo, Zpět, Potvrdit"
STR_FRONT_LAYOUT_LBCR: "Vlevo, Zpět, Potvrdit, Vpravo"
STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Malý"
STR_MEDIUM: "Střední"
STR_LARGE: "Velký"
STR_X_LARGE: "Obří"
STR_TIGHT: "Těsný"
STR_NORMAL: "Normální"
STR_WIDE: "Široký"
STR_JUSTIFY: "Zarovnat do bloku"
STR_ALIGN_LEFT: "Vlevo"
STR_CENTER: "Na střed"
STR_ALIGN_RIGHT: "Vpravo"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 stránka"
STR_PAGES_5: "5 stránek"
STR_PAGES_10: "10 stránek"
STR_PAGES_15: "15 stránek"
STR_PAGES_30: "30 stránek"
STR_UPDATE: "Aktualizace"
STR_CHECKING_UPDATE: "Kontrola aktualizací…"
STR_NEW_UPDATE: "Nová aktualizace k dispozici!"
STR_CURRENT_VERSION: "Aktuální verze:"
STR_NEW_VERSION: "Nová verze:"
STR_UPDATING: "Aktualizace..."
STR_NO_UPDATE: "Žádná aktualizace k dispozici"
STR_UPDATE_FAILED: "Aktualizace selhala"
STR_UPDATE_COMPLETE: "Aktualizace dokončena"
STR_POWER_ON_HINT: "Stiskněte a podržte tlačítko napájení pro opětovné zapnutí"
STR_EXTERNAL_FONT: "Externí písmo"
STR_BUILTIN_DISABLED: "Vestavěné (Zakázáno)"
STR_NO_ENTRIES: "Žádné položky nenalezeny"
STR_DOWNLOADING: "Stahování..."
STR_DOWNLOAD_FAILED: "Stahování selhalo"
STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepojmenované"
STR_NO_SERVER_URL: "Není nakonfigurována adresa URL serveru"
STR_FETCH_FEED_FAILED: "Načtení kanálu se nezdařilo"
STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo"
STR_NETWORK_PREFIX: "Síť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_SCAN_QR_WIFI_HINT: "nebo naskenujte QR kód telefonem pro připojení k Wi-Fi."
STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení"
STR_SD_CARD: "SD karta"
STR_BACK: "« Zpět"
STR_EXIT: "« Konec"
STR_HOME: "« Domů"
STR_SAVE: "« Uložit"
STR_SELECT: "Vybrat"
STR_TOGGLE: "Přepnout"
STR_CONFIRM: "Potvrdit"
STR_CANCEL: "Zrušit"
STR_CONNECT: "Připojit"
STR_OPEN: "Otevřít"
STR_DOWNLOAD: "Stáhnout"
STR_RETRY: "Zkusit znovu"
STR_YES: "Ano"
STR_NO: "Ne"
STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP"
STR_SET: "Nastavit"
STR_NOT_SET: "Nenastaveno"
STR_DIR_LEFT: "Vlevo"
STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Nahoru"
STR_DIR_DOWN: "Dolů"
STR_CAPS_ON: "PÍSMO"
STR_CAPS_OFF: "písmo"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ZAP]"
STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Plný s procenty"
STR_STATUS_BAR_FULL_BOOK: "Plný s pruhem knih"
STR_STATUS_BAR_BOOK_ONLY: "Pouze pruh knih"
STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol"
STR_UI_THEME: "Šablona rozhraní"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
STR_OPDS_BROWSER: "Prohlížeč OPDS"
STR_COVER_CUSTOM: "Obálka + Vlastní"
STR_RECENTS: "Nedávné"
STR_MENU_RECENT_BOOKS: "Nedávné knihy"
STR_NO_RECENT_BOOKS: "Žádné nedávné knihy"
STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre"
STR_FORGET_AND_REMOVE: "Zapomenout síť a odstranit uložené heslo?"
STR_FORGET_BUTTON: "Zapomenout na síť"
STR_CALIBRE_STARTING: "Spuštění Calibre..."
STR_CALIBRE_SETUP: "Nastavení"
STR_CALIBRE_STATUS: "Stav"
STR_CLEAR_BUTTON: "Vymazat"
STR_DEFAULT_VALUE: "Výchozí"
STR_REMAP_PROMPT: "Stiskněte přední tlačítko pro každou roli"
STR_UNASSIGNED: "Nepřiřazeno"
STR_ALREADY_ASSIGNED: "Již přiřazeno"
STR_REMAP_RESET_HINT: "Boční tlačítko Nahoru: Obnovit výchozí rozvržení"
STR_REMAP_CANCEL_HINT: "Boční tlačítko Dolů: Zrušit přemapování"
STR_HW_BACK_LABEL: "Zpět (1. tlačítko)"
STR_HW_CONFIRM_LABEL: "Potvrdit (2. tlačítko)"
STR_HW_LEFT_LABEL: "Vlevo (3. tlačítko)"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačítko)"
STR_GO_TO_PERCENT: "Přejít na %"
STR_GO_HOME_BUTTON: "Přejít Domů"
STR_SYNC_PROGRESS: "Průběh synchronizace"
STR_DELETE_CACHE: "Smazat mezipaměť knihy"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |"
STR_BOOK_PREFIX: "Kniha:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "ZÁMEK"
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_CALC_HASH: "Výpočet hashe dokumentu..."
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
STR_FETCH_PROGRESS: "Načítání vzdáleného průběhu..."
STR_UPLOAD_PROGRESS: "Průběh nahrávání..."
STR_NO_CREDENTIALS_MSG: "Přihlašovací údaje nejsou nakonfigurovány"
STR_KOREADER_SETUP_HINT: "Nastavit účet KOReader v Nastavení"
STR_PROGRESS_FOUND: "Nalezen průběh!"
STR_REMOTE_LABEL: "Vzdálené:"
STR_LOCAL_LABEL: "Lokální:"
STR_PAGE_OVERALL_FORMAT: "Stránka %d, celkově %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stránka %d/%d, celkově %.2f%%"
STR_DEVICE_FROM_FORMAT: " Od: %s"
STR_APPLY_REMOTE: "Použít vzdálený postup"
STR_UPLOAD_LOCAL: "Nahrát lokální postup"
STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený postup"
STR_UPLOAD_PROMPT: "Nahrát aktuální pozici?"
STR_UPLOAD_SUCCESS: "Postup nahrán!"
STR_SYNC_FAILED_MSG: "Synchronizace se nezdařila"
STR_SECTION_PREFIX: "Sekce"
STR_UPLOAD: "Nahrát"
STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl"
STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_CHOOSE_SOMETHING: "Vyberte si něco ke čtení"
STR_CLOCK: "Hodiny"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 hodin"
STR_SET_TIME: "Nastavit čas"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,358 @@
_language_name: "English"
_language_code: "ENGLISH"
_order: "0"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BOOTING"
STR_SLEEPING: "SLEEPING"
STR_ENTERING_SLEEP: "Entering Sleep..."
STR_BROWSE_FILES: "Browse Files"
STR_FILE_TRANSFER: "File Transfer"
STR_SETTINGS_TITLE: "Settings"
STR_CALIBRE_LIBRARY: "Calibre Library"
STR_CONTINUE_READING: "Continue Reading"
STR_NO_OPEN_BOOK: "No open book"
STR_START_READING: "Start reading below"
STR_BOOKS: "Books"
STR_NO_BOOKS_FOUND: "No books found"
STR_SELECT_CHAPTER: "Select Chapter"
STR_NO_CHAPTERS: "No chapters"
STR_END_OF_BOOK: "End of book"
STR_EMPTY_CHAPTER: "Empty chapter"
STR_INDEXING: "Indexing..."
STR_MEMORY_ERROR: "Memory error"
STR_PAGE_LOAD_ERROR: "Page load error"
STR_EMPTY_FILE: "Empty file"
STR_OUT_OF_BOUNDS: "Out of bounds"
STR_LOADING: "Loading..."
STR_LOAD_XTC_FAILED: "Failed to load XTC"
STR_LOAD_TXT_FAILED: "Failed to load TXT"
STR_LOAD_EPUB_FAILED: "Failed to load EPUB"
STR_SD_CARD_ERROR: "SD card error"
STR_WIFI_NETWORKS: "WiFi Networks"
STR_NO_NETWORKS: "No networks found"
STR_NETWORKS_FOUND: "%zu networks found"
STR_SCANNING: "Scanning..."
STR_CONNECTING: "Connecting..."
STR_CONNECTED: "Connected!"
STR_CONNECTION_FAILED: "Connection Failed"
STR_CONNECTION_TIMEOUT: "Connection timeout"
STR_FORGET_NETWORK: "Forget Network?"
STR_SAVE_PASSWORD: "Save password for next time?"
STR_REMOVE_PASSWORD: "Remove saved password?"
STR_PRESS_OK_SCAN: "Press OK to scan again"
STR_PRESS_ANY_CONTINUE: "Press any button to continue"
STR_SELECT_HINT: "LEFT/RIGHT: Select | OK: Confirm"
STR_HOW_CONNECT: "How would you like to connect?"
STR_JOIN_NETWORK: "Join a Network"
STR_CREATE_HOTSPOT: "Create Hotspot"
STR_JOIN_DESC: "Connect to an existing WiFi network"
STR_HOTSPOT_DESC: "Create a WiFi network others can join"
STR_STARTING_HOTSPOT: "Starting Hotspot..."
STR_HOTSPOT_MODE: "Hotspot Mode"
STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network"
STR_OPEN_URL_HINT: "Open this URL in your browser"
STR_OR_HTTP_PREFIX: "or http://"
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CONNECT_WIRELESS: "Connect as Wireless Device"
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
STR_MAC_ADDRESS: "MAC address:"
STR_CHECKING_WIFI: "Checking WiFi..."
STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password"
STR_ENTER_TEXT: "Enter Text"
STR_TO_PREFIX: "to "
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
STR_CALIBRE_CONNECTING_TO: "Connecting to "
STR_CALIBRE_CONNECTED_TO: "Connected to "
STR_CALIBRE_WAITING_COMMANDS: "Waiting for commands..."
STR_CONNECTION_FAILED_RETRYING: "(Connection failed, retrying)"
STR_CALIBRE_DISCONNECTED: "Calibre disconnected"
STR_CALIBRE_WAITING_TRANSFER: "Waiting for transfer..."
STR_CALIBRE_TRANSFER_HINT: "If transfer fails, enable\\n'Ignore free space' in Calibre's\\nSmartDevice plugin settings."
STR_CALIBRE_RECEIVING: "Receiving: "
STR_CALIBRE_RECEIVED: "Received: "
STR_CALIBRE_WAITING_MORE: "Waiting for more..."
STR_CALIBRE_FAILED_CREATE_FILE: "Failed to create file"
STR_CALIBRE_PASSWORD_REQUIRED: "Password required"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer interrupted"
STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\""
STR_CAT_DISPLAY: "Display"
STR_CAT_READER: "Reader"
STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "System"
STR_CAT_CLOCK: "Clock"
STR_SLEEP_SCREEN: "Sleep Screen"
STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
STR_STATUS_BAR: "Status Bar"
STR_HIDE_BATTERY: "Hide Battery %"
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
STR_TEXT_AA: "Text Anti-Aliasing"
STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_FRONT_BTN_LAYOUT: "Front Button Layout"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
STR_FONT_FAMILY: "Reader Font Family"
STR_EXT_READER_FONT: "External Reader Font"
STR_EXT_CHINESE_FONT: "Reader Font"
STR_EXT_UI_FONT: "UI Font"
STR_FONT_SIZE: "UI Font Size"
STR_LINE_SPACING: "Reader Line Spacing"
STR_ASCII_LETTER_SPACING: "ASCII Letter Spacing"
STR_ASCII_DIGIT_SPACING: "ASCII Digit Spacing"
STR_CJK_SPACING: "CJK Spacing"
STR_COLOR_MODE: "Color Mode"
STR_SCREEN_MARGIN: "Reader Screen Margin"
STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
STR_HYPHENATION: "Hyphenation"
STR_TIME_TO_SLEEP: "Time to Sleep"
STR_REFRESH_FREQ: "Refresh Frequency"
STR_CALIBRE_SETTINGS: "Calibre Settings"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Check for updates"
STR_LANGUAGE: "Language"
STR_SELECT_WALLPAPER: "Select Wallpaper"
STR_CLEAR_READING_CACHE: "Clear Reading Cache"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Username"
STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL"
STR_DOCUMENT_MATCHING: "Document Matching"
STR_AUTHENTICATE: "Authenticate"
STR_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password"
STR_FILENAME: "Filename"
STR_BINARY: "Binary"
STR_SET_CREDENTIALS_FIRST: "Set credentials first"
STR_WIFI_CONN_FAILED: "WiFi connection failed"
STR_AUTHENTICATING: "Authenticating..."
STR_AUTH_SUCCESS: "Successfully authenticated!"
STR_KOREADER_AUTH: "KOReader Auth"
STR_SYNC_READY: "KOReader sync is ready to use"
STR_AUTH_FAILED: "Authentication Failed"
STR_DONE: "Done"
STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data."
STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!"
STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed"
STR_CLEAR_CACHE_WARNING_4: "when opened again."
STR_CLEARING_CACHE: "Clearing cache..."
STR_CACHE_CLEARED: "Cache Cleared"
STR_ITEMS_REMOVED: "items removed"
STR_FAILED_LOWER: "failed"
STR_CLEAR_CACHE_FAILED: "Failed to clear cache"
STR_CHECK_SERIAL_OUTPUT: "Check serial output for details"
STR_DARK: "Dark"
STR_LIGHT: "Light"
STR_CUSTOM: "Custom"
STR_COVER: "Cover"
STR_NONE_OPT: "None"
STR_FIT: "Fit"
STR_CROP: "Crop"
STR_NO_PROGRESS: "No Progress"
STR_FULL_OPT: "Full"
STR_NEVER: "Never"
STR_IN_READER: "In Reader"
STR_ALWAYS: "Always"
STR_IGNORE: "Ignore"
STR_SLEEP: "Sleep"
STR_PAGE_TURN: "Page Turn"
STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted"
STR_LANDSCAPE_CCW: "Landscape CCW"
STR_FRONT_LAYOUT_BCLR: "Bck, Cnfrm, Lft, Rght"
STR_FRONT_LAYOUT_LRBC: "Lft, Rght, Bck, Cnfrm"
STR_FRONT_LAYOUT_LBCR: "Lft, Bck, Cnfrm, Rght"
STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Small"
STR_MEDIUM: "Medium"
STR_LARGE: "Large"
STR_X_LARGE: "X Large"
STR_TIGHT: "Tight"
STR_NORMAL: "Normal"
STR_WIDE: "Wide"
STR_JUSTIFY: "Justify"
STR_ALIGN_LEFT: "Left"
STR_CENTER: "Center"
STR_ALIGN_RIGHT: "Right"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 page"
STR_PAGES_5: "5 pages"
STR_PAGES_10: "10 pages"
STR_PAGES_15: "15 pages"
STR_PAGES_30: "30 pages"
STR_UPDATE: "Update"
STR_CHECKING_UPDATE: "Checking for update..."
STR_NEW_UPDATE: "New update available!"
STR_CURRENT_VERSION: "Current Version: "
STR_NEW_VERSION: "New Version: "
STR_UPDATING: "Updating..."
STR_NO_UPDATE: "No update available"
STR_UPDATE_FAILED: "Update failed"
STR_UPDATE_COMPLETE: "Update complete"
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
STR_EXTERNAL_FONT: "External Font"
STR_BUILTIN_DISABLED: "Built-in (Disabled)"
STR_NO_ENTRIES: "No entries found"
STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed"
STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Unnamed"
STR_NO_SERVER_URL: "No server URL configured"
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_NETWORK_PREFIX: "Network: "
STR_IP_ADDRESS_PREFIX: "IP Address: "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
STR_ERROR_NETWORK_NOT_FOUND: "Error: Network not found"
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
STR_SD_CARD: "SD card"
STR_BACK: "« Back"
STR_EXIT: "« Exit"
STR_HOME: "« Home"
STR_SAVE: "« Save"
STR_SELECT: "Select"
STR_TOGGLE: "Toggle"
STR_CONFIRM: "Confirm"
STR_CANCEL: "Cancel"
STR_CONNECT: "Connect"
STR_OPEN: "Open"
STR_DOWNLOAD: "Download"
STR_RETRY: "Retry"
STR_YES: "Yes"
STR_NO: "No"
STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF"
STR_SET: "Set"
STR_NOT_SET: "Not Set"
STR_DIR_LEFT: "Left"
STR_DIR_RIGHT: "Right"
STR_DIR_UP: "Up"
STR_DIR_DOWN: "Down"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ON]"
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
STR_FILTER_CONTRAST: "Contrast"
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Percentage"
STR_STATUS_BAR_FULL_BOOK: "Full w/ Book Bar"
STR_STATUS_BAR_BOOK_ONLY: "Book Bar Only"
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser"
STR_COVER_CUSTOM: "Cover + Custom"
STR_RECENTS: "Recents"
STR_MENU_RECENT_BOOKS: "Recent Books"
STR_NO_RECENT_BOOKS: "No recent books"
STR_CALIBRE_DESC: "Use Calibre wireless device transfers"
STR_FORGET_AND_REMOVE: "Forget network and remove saved password?"
STR_FORGET_BUTTON: "Forget network"
STR_CALIBRE_STARTING: "Starting Calibre..."
STR_CALIBRE_SETUP: "Setup"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Clear"
STR_DEFAULT_VALUE: "Default"
STR_REMAP_PROMPT: "Press a front button for each role"
STR_UNASSIGNED: "Unassigned"
STR_ALREADY_ASSIGNED: "Already assigned"
STR_REMAP_RESET_HINT: "Side button Up: Reset to default layout"
STR_REMAP_CANCEL_HINT: "Side button Down: Cancel remapping"
STR_HW_BACK_LABEL: "Back (1st button)"
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
STR_HW_LEFT_LABEL: "Left (3rd button)"
STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Reading Progress"
STR_DELETE_CACHE: "Delete Book Cache"
STR_CHAPTER_PREFIX: "Chapter: "
STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Book: "
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
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_CALC_HASH: "Calculating document hash..."
STR_HASH_FAILED: "Failed to calculate document hash"
STR_FETCH_PROGRESS: "Fetching remote progress..."
STR_UPLOAD_PROGRESS: "Uploading progress..."
STR_NO_CREDENTIALS_MSG: "No credentials configured"
STR_KOREADER_SETUP_HINT: "Set up KOReader account in Settings"
STR_PROGRESS_FOUND: "Progress found!"
STR_REMOTE_LABEL: "Remote:"
STR_LOCAL_LABEL: "Local:"
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% overall"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% overall"
STR_DEVICE_FROM_FORMAT: " From: %s"
STR_APPLY_REMOTE: "Apply remote progress"
STR_UPLOAD_LOCAL: "Upload local progress"
STR_NO_REMOTE_MSG: "No remote progress found"
STR_UPLOAD_PROMPT: "Upload current position?"
STR_UPLOAD_SUCCESS: "Progress uploaded!"
STR_SYNC_FAILED_MSG: "Sync failed"
STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Book's Style"
STR_EMBEDDED_STYLE: "Embedded Style"
STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_LETTERBOX_FILL: "Letterbox Fill"
STR_DITHERED: "Dithered"
STR_SOLID: "Solid"
STR_ADD_BOOKMARK: "Add Bookmark"
STR_REMOVE_BOOKMARK: "Remove Bookmark"
STR_LOOKUP_WORD: "Lookup Word"
STR_LOOKUP_HISTORY: "Lookup Word History"
STR_GO_TO_BOOKMARK: "Go to Bookmark"
STR_CLOSE_BOOK: "Close Book"
STR_DELETE_DICT_CACHE: "Delete Dictionary Cache"
STR_DEFAULT_OPTION: "Default"
STR_BOOKMARK_ADDED: "Bookmark added"
STR_BOOKMARK_REMOVED: "Bookmark removed"
STR_DICT_CACHE_DELETED: "Dictionary cache deleted"
STR_NO_CACHE_TO_DELETE: "No cache to delete"
STR_TABLE_OF_CONTENTS: "Table of Contents"
STR_TOGGLE_ORIENTATION: "Toggle Portrait/Landscape"
STR_TOGGLE_FONT_SIZE: "Toggle Font Size"
STR_OVERRIDE_LETTERBOX_FILL: "Override Letterbox Fill"
STR_PREFERRED_PORTRAIT: "Preferred Portrait"
STR_PREFERRED_LANDSCAPE: "Preferred Landscape"
STR_CHOOSE_SOMETHING: "Choose something to read"
STR_CLOCK: "Clock"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 Hour"
STR_SET_TIME: "Set Time"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,337 @@
_language_name: "Français"
_language_code: "FRENCH"
_order: "2"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "DÉMARRAGE EN COURS"
STR_SLEEPING: "VEILLE"
STR_ENTERING_SLEEP: "Mise en veille…"
STR_BROWSE_FILES: "Fichiers"
STR_FILE_TRANSFER: "Transfert"
STR_SETTINGS_TITLE: "Réglages"
STR_CALIBRE_LIBRARY: "Bibliothèque Calibre"
STR_CONTINUE_READING: "Continuer la lecture"
STR_NO_OPEN_BOOK: "Aucun livre ouvert"
STR_START_READING: "Lisez votre premier livre ci-dessous"
STR_BOOKS: "Livres"
STR_NO_BOOKS_FOUND: "Dossier vide"
STR_SELECT_CHAPTER: "Choix du chapitre"
STR_NO_CHAPTERS: "Aucun chapitre"
STR_END_OF_BOOK: "Fin du livre"
STR_EMPTY_CHAPTER: "Chapitre vide"
STR_INDEXING: "Indexation en cours…"
STR_MEMORY_ERROR: "Erreur de mémoire"
STR_PAGE_LOAD_ERROR: "Erreur de chargement"
STR_EMPTY_FILE: "Fichier vide"
STR_OUT_OF_BOUNDS: "Dépassement de mémoire"
STR_LOADING: "Chargement…"
STR_LOAD_XTC_FAILED: "Erreur de chargement du fichier XTC"
STR_LOAD_TXT_FAILED: "Erreur de chargement du fichier TXT"
STR_LOAD_EPUB_FAILED: "Erreur de chargement du fichier EPUB"
STR_SD_CARD_ERROR: "Carte mémoire absente"
STR_WIFI_NETWORKS: "Réseaux WiFi"
STR_NO_NETWORKS: "Aucun réseau"
STR_NETWORKS_FOUND: "%zu réseaux"
STR_SCANNING: "Recherche de réseaux en cours…"
STR_CONNECTING: "Connexion en cours…"
STR_CONNECTED: "Connecté !"
STR_CONNECTION_FAILED: "Échec de la connexion"
STR_CONNECTION_TIMEOUT: "Délai de connexion dépassé"
STR_FORGET_NETWORK: "Oublier ce réseau ?"
STR_SAVE_PASSWORD: "Enregistrer le mot de passe ?"
STR_REMOVE_PASSWORD: "Supprimer le mot de passe enregistré ?"
STR_PRESS_OK_SCAN: "Appuyez sur OK pour détecter à nouveau"
STR_PRESS_ANY_CONTINUE: "Appuyez sur une touche pour continuer"
STR_SELECT_HINT: "GAUCHE/DROITE: Sélectionner | OK: Valider"
STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?"
STR_JOIN_NETWORK: "Connexion à un réseau"
STR_CREATE_HOTSPOT: "Créer un point daccès"
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis dautres appareils"
STR_STARTING_HOTSPOT: "Création du point daccès en cours…"
STR_HOTSPOT_MODE: "Mode point daccès"
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi"
STR_OPEN_URL_HINT: "Ouvrez cette URL dans votre navigateur"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou scannez le QR code avec votre téléphone"
STR_CALIBRE_WIRELESS: "Connexion à Calibre sans fil"
STR_CALIBRE_WEB_URL: "URL Web Calibre"
STR_CONNECT_WIRELESS: "Se connecter comme appareil sans fil"
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
STR_MAC_ADDRESS: "Adresse MAC :"
STR_CHECKING_WIFI: "Vérification du réseau WiFi..."
STR_ENTER_WIFI_PASSWORD: "Entrez le mot de passe WiFi"
STR_ENTER_TEXT: "Entrez le texte"
STR_TO_PREFIX: "à "
STR_CALIBRE_DISCOVERING: "Recherche de Calibre en cours…"
STR_CALIBRE_CONNECTING_TO: "Connexion à "
STR_CALIBRE_CONNECTED_TO: "Connecté à "
STR_CALIBRE_WAITING_COMMANDS: "En attente de commandes…"
STR_CONNECTION_FAILED_RETRYING: "(Échec de la connexion, nouvelle tentative)"
STR_CALIBRE_DISCONNECTED: "Calibre déconnecté"
STR_CALIBRE_WAITING_TRANSFER: "En attente de transfert…"
STR_CALIBRE_TRANSFER_HINT: "Si le transfert échoue, activez\\nIgnorer lespace libre dans les\\nparamètres du plugin SmartDevice de Calibre."
STR_CALIBRE_RECEIVING: "Réception : "
STR_CALIBRE_RECEIVED: "Reçus : "
STR_CALIBRE_WAITING_MORE: "En attente de données supplémentaires…"
STR_CALIBRE_FAILED_CREATE_FILE: "Échec de la création du fichier"
STR_CALIBRE_PASSWORD_REQUIRED: "Mot de passe requis"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu"
STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : Envoyer vers lappareil"
STR_CALIBRE_INSTRUCTION_4: "“Gardez cet écran ouvert pendant le transfert”"
STR_CAT_DISPLAY: "Affichage"
STR_CAT_READER: "Lecteur"
STR_CAT_CONTROLS: "Commandes"
STR_CAT_SYSTEM: "Système"
STR_CAT_CLOCK: "Horloge"
STR_SLEEP_SCREEN: "Écran de veille"
STR_SLEEP_COVER_MODE: "Mode dimage de lécran de veille"
STR_STATUS_BAR: "Barre détat"
STR_HIDE_BATTERY: "Masquer % batterie"
STR_EXTRA_SPACING: "Espacement des paragraphes"
STR_TEXT_AA: "Lissage du texte"
STR_SHORT_PWR_BTN: "Appui court bout. alim."
STR_ORIENTATION: "Orientation de lecture"
STR_FRONT_BTN_LAYOUT: "Disposition des boutons avant"
STR_SIDE_BTN_LAYOUT: "Disposition des boutons latéraux"
STR_LONG_PRESS_SKIP: "Appui long pour saut de chapitre"
STR_FONT_FAMILY: "Police de caractères du lecteur"
STR_EXT_READER_FONT: "Police externe"
STR_EXT_CHINESE_FONT: "Police du lecteur"
STR_EXT_UI_FONT: "Police de linterface"
STR_FONT_SIZE: "Taille du texte de linterface"
STR_LINE_SPACING: "Espacement des lignes"
STR_ASCII_LETTER_SPACING: "Espacement des lettres ASCII"
STR_ASCII_DIGIT_SPACING: "Espacement des chiffres ASCII"
STR_CJK_SPACING: "Espacement CJK"
STR_COLOR_MODE: "Mode couleur"
STR_SCREEN_MARGIN: "Marges du lecteur"
STR_PARA_ALIGNMENT: "Alignement des paragraphes"
STR_HYPHENATION: "Césure"
STR_TIME_TO_SLEEP: "Mise en veille automatique"
STR_REFRESH_FREQ: "Fréquence de rafraîchissement"
STR_CALIBRE_SETTINGS: "Réglages Calibre"
STR_KOREADER_SYNC: "Synchronisation KOReader"
STR_CHECK_UPDATES: "Mise à jour"
STR_LANGUAGE: "Langue"
STR_SELECT_WALLPAPER: "Fond décran"
STR_CLEAR_READING_CACHE: "Vider le cache de lecture"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nom dutilisateur"
STR_PASSWORD: "Mot de passe"
STR_SYNC_SERVER_URL: "URL du serveur"
STR_DOCUMENT_MATCHING: "Correspondance"
STR_AUTHENTICATE: "Se connecter"
STR_KOREADER_USERNAME: "Nom dutilisateur"
STR_KOREADER_PASSWORD: "Mot de passe"
STR_FILENAME: "Nom de fichier"
STR_BINARY: "Binaire"
STR_SET_CREDENTIALS_FIRST: "Identifiants manquants"
STR_WIFI_CONN_FAILED: "Échec de connexion WiFi"
STR_AUTHENTICATING: "Connexion en cours…"
STR_AUTH_SUCCESS: "Connexion réussie !"
STR_KOREADER_AUTH: "Auth KOReader"
STR_SYNC_READY: "Synchronisation KOReader prête"
STR_AUTH_FAILED: "Échec de la connexion"
STR_DONE: "OK"
STR_CLEAR_CACHE_WARNING_1: "Le cache de votre bibliothèque sera entièrement vidé"
STR_CLEAR_CACHE_WARNING_2: "Votre progression de lecture sera perdue !"
STR_CLEAR_CACHE_WARNING_3: "Les livres devront être réindexés"
STR_CLEAR_CACHE_WARNING_4: "à leur prochaine ouverture."
STR_CLEARING_CACHE: "Suppression du cache…"
STR_CACHE_CLEARED: "Cache supprimé"
STR_ITEMS_REMOVED: "éléments supprimés"
STR_FAILED_LOWER: "ont échoué"
STR_CLEAR_CACHE_FAILED: "Échec de la suppression du cache"
STR_CHECK_SERIAL_OUTPUT: "Vérifiez la console série pour plus de détails"
STR_DARK: "Sombre"
STR_LIGHT: "Clair"
STR_CUSTOM: "Custom"
STR_COVER: "Couverture"
STR_NONE_OPT: "Aucun"
STR_FIT: "Ajusté"
STR_CROP: "Rogné"
STR_NO_PROGRESS: "Sans progression"
STR_FULL_OPT: "Complète"
STR_NEVER: "Jamais"
STR_IN_READER: "Dans le lecteur"
STR_ALWAYS: "Toujours"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Mise en veille"
STR_PAGE_TURN: "Page suivante"
STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Paysage"
STR_INVERTED: "Inversé"
STR_LANDSCAPE_CCW: "Paysage inversé"
STR_FRONT_LAYOUT_BCLR: "Ret, OK, Gauche, Droite"
STR_FRONT_LAYOUT_LRBC: "Gauche, Droite, Ret, OK"
STR_FRONT_LAYOUT_LBCR: "Gauche, Ret, OK, Droite"
STR_PREV_NEXT: "Prec/Suiv"
STR_NEXT_PREV: "Suiv/Prec"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Petite"
STR_MEDIUM: "Moyenne"
STR_LARGE: "Grande"
STR_X_LARGE: "T Grande"
STR_TIGHT: "Serré"
STR_NORMAL: "Normal"
STR_WIDE: "Large"
STR_JUSTIFY: "Justifier"
STR_ALIGN_LEFT: "Gauche"
STR_CENTER: "Centre"
STR_ALIGN_RIGHT: "Droite"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 page"
STR_PAGES_5: "5 pages"
STR_PAGES_10: "10 pages"
STR_PAGES_15: "15 pages"
STR_PAGES_30: "30 pages"
STR_UPDATE: "Mise à jour"
STR_CHECKING_UPDATE: "Recherche de mises à jour en cours…"
STR_NEW_UPDATE: "Nouvelle mise à jour disponible !"
STR_CURRENT_VERSION: "Version actuelle :"
STR_NEW_VERSION: "Nouvelle version : "
STR_UPDATING: "Mise à jour en cours…"
STR_NO_UPDATE: "Aucune mise à jour disponible"
STR_UPDATE_FAILED: "Échec de la mise à jour"
STR_UPDATE_COMPLETE: "Mise à jour effectuée"
STR_POWER_ON_HINT: "Maintenir le bouton dalimentation pour redémarrer"
STR_EXTERNAL_FONT: "Police externe"
STR_BUILTIN_DISABLED: "Intégrée (désactivée)"
STR_NO_ENTRIES: "Aucune entrée trouvée"
STR_DOWNLOADING: "Téléchargement en cours…"
STR_DOWNLOAD_FAILED: "Échec du téléchargement"
STR_ERROR_MSG: "Erreur : "
STR_UNNAMED: "Sans titre"
STR_NO_SERVER_URL: "Aucune URL serveur configurée"
STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux"
STR_PARSE_FEED_FAILED: "Échec de lanalyse du flux"
STR_NETWORK_PREFIX: "Réseau : "
STR_IP_ADDRESS_PREFIX: "Adresse IP : "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général"
STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable"
STR_ERROR_CONNECTION_TIMEOUT: "Erreur : Délai de connexion dépassé"
STR_SD_CARD: "Carte SD"
STR_BACK: "« Retour"
STR_EXIT: "« Sortie"
STR_HOME: "« Accueil"
STR_SAVE: "« Sauver"
STR_SELECT: "OK"
STR_TOGGLE: "Modifier"
STR_CONFIRM: "Confirmer"
STR_CANCEL: "Annuler"
STR_CONNECT: "OK"
STR_OPEN: "Ouvrir"
STR_DOWNLOAD: "Télécharger"
STR_RETRY: "Réessayer"
STR_YES: "Oui"
STR_NO: "Non"
STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF"
STR_SET: "Défini"
STR_NOT_SET: "Non défini"
STR_DIR_LEFT: "Gauche"
STR_DIR_RIGHT: "Droite"
STR_DIR_UP: "Haut"
STR_DIR_DOWN: "Bas"
STR_CAPS_ON: "MAJ"
STR_CAPS_OFF: "maj"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ON]"
STR_SLEEP_COVER_FILTER: "Filtre affichage veille"
STR_FILTER_CONTRAST: "Contraste"
STR_STATUS_BAR_FULL_PERCENT: "Complète + %"
STR_STATUS_BAR_FULL_BOOK: "Complète + barre livre"
STR_STATUS_BAR_BOOK_ONLY: "Barre livre"
STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre"
STR_UI_THEME: "Thème de linterface"
STR_THEME_CLASSIC: "Classique"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil"
STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant"
STR_OPDS_BROWSER: "Navigateur OPDS"
STR_COVER_CUSTOM: "Couverture + Custom"
STR_RECENTS: "Récents"
STR_MENU_RECENT_BOOKS: "Livres récents"
STR_NO_RECENT_BOOKS: "Aucun livre récent"
STR_CALIBRE_DESC: "Utiliser les transferts sans fil Calibre"
STR_FORGET_AND_REMOVE: "Oublier le réseau et supprimer le mot de passe enregistré ?"
STR_FORGET_BUTTON: "Oublier le réseau"
STR_CALIBRE_STARTING: "Démarrage de Calibre..."
STR_CALIBRE_SETUP: "Configuration"
STR_CALIBRE_STATUS: "Statut"
STR_CLEAR_BUTTON: "Effacer"
STR_DEFAULT_VALUE: "Défaut"
STR_REMAP_PROMPT: "Appuyez sur un bouton avant pour chaque rôle"
STR_UNASSIGNED: "Non assigné"
STR_ALREADY_ASSIGNED: "Déjà assigné"
STR_REMAP_RESET_HINT: "Bouton latéral haut : Réinitialiser"
STR_REMAP_CANCEL_HINT: "Bouton latéral bas : Annuler le réglage"
STR_HW_BACK_LABEL: "Retour (1er bouton)"
STR_HW_CONFIRM_LABEL: "OK (2ème bouton)"
STR_HW_LEFT_LABEL: "Gauche (3ème bouton)"
STR_HW_RIGHT_LABEL: "Droite (4ème bouton)"
STR_GO_TO_PERCENT: "Aller à %"
STR_GO_HOME_BUTTON: "Aller à laccueil"
STR_SYNC_PROGRESS: "Synchroniser la progression"
STR_DELETE_CACHE: "Supprimer le cache du livre"
STR_CHAPTER_PREFIX: "Chapitre : "
STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Livre : "
STR_KBD_SHIFT: "maj"
STR_KBD_SHIFT_CAPS: "MAJ"
STR_KBD_LOCK: "VERR MAJ"
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à lURL"
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
STR_SYNCING_TIME: "Synchronisation de lheure…"
STR_CALC_HASH: "Calcul du hash du document…"
STR_HASH_FAILED: "Échec du calcul du hash du document"
STR_FETCH_PROGRESS: "Téléchargement de la progression…"
STR_UPLOAD_PROGRESS: "Envoi de la progression…"
STR_NO_CREDENTIALS_MSG: "Aucun identifiant configuré"
STR_KOREADER_SETUP_HINT: "Configurez le compte KOReader dans les réglages"
STR_PROGRESS_FOUND: "Progression trouvée !"
STR_REMOTE_LABEL: "En ligne :"
STR_LOCAL_LABEL: "Locale :"
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% au total"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% au total"
STR_DEVICE_FROM_FORMAT: " De : %s"
STR_APPLY_REMOTE: "Appliquer la progression en ligne"
STR_UPLOAD_LOCAL: "Envoyer la progression locale"
STR_NO_REMOTE_MSG: "Aucune progression en ligne trouvée"
STR_UPLOAD_PROMPT: "Envoyer la position actuelle ?"
STR_UPLOAD_SUCCESS: "Progression envoyée !"
STR_SYNC_FAILED_MSG: "Échec de la synchronisation"
STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Envoi"
STR_BOOK_S_STYLE: "Style du livre"
STR_EMBEDDED_STYLE: "Style intégré"
STR_OPDS_SERVER_URL: "URL du serveur OPDS"
STR_CHOOSE_SOMETHING: "Choisissez quelque chose à lire"
STR_CLOCK: "Horloge"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 heures"
STR_SET_TIME: "Régler l'heure"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,337 @@
_language_name: "Deutsch"
_language_code: "GERMAN"
_order: "3"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "STARTEN"
STR_SLEEPING: "STANDBY"
STR_ENTERING_SLEEP: "Standby..."
STR_BROWSE_FILES: "Durchsuchen"
STR_FILE_TRANSFER: "Datentransfer"
STR_SETTINGS_TITLE: "Einstellungen"
STR_CALIBRE_LIBRARY: "Calibre-Bibliothek"
STR_CONTINUE_READING: "Weiterlesen"
STR_NO_OPEN_BOOK: "Aktuell kein Buch"
STR_START_READING: "Lesen beginnen"
STR_BOOKS: "Bücher"
STR_NO_BOOKS_FOUND: "Keine Bücher"
STR_SELECT_CHAPTER: "Kapitel auswählen"
STR_NO_CHAPTERS: "Keine Kapitel"
STR_END_OF_BOOK: "Buchende"
STR_EMPTY_CHAPTER: "Kapitelende"
STR_INDEXING: "Indexieren…"
STR_MEMORY_ERROR: "Speicherfehler"
STR_PAGE_LOAD_ERROR: "Seitenladefehler"
STR_EMPTY_FILE: "Leere Datei"
STR_OUT_OF_BOUNDS: "Zu groß"
STR_LOADING: "Laden…"
STR_LOAD_XTC_FAILED: "Ladefehler bei XTC"
STR_LOAD_TXT_FAILED: "Ladefehler bei TXT"
STR_LOAD_EPUB_FAILED: "Ladefehler bei EPUB"
STR_SD_CARD_ERROR: "SD-Karten-Fehler"
STR_WIFI_NETWORKS: "WLAN-Netzwerke"
STR_NO_NETWORKS: "Kein WLAN gefunden"
STR_NETWORKS_FOUND: "%zu WLAN-Netzwerke gefunden"
STR_SCANNING: "Suchen..."
STR_CONNECTING: "Verbinden..."
STR_CONNECTED: "Verbunden!"
STR_CONNECTION_FAILED: "Verbindungsfehler"
STR_CONNECTION_TIMEOUT: "Verbindungs-Timeout"
STR_FORGET_NETWORK: "WLAN vergessen?"
STR_SAVE_PASSWORD: "Passwort speichern?"
STR_REMOVE_PASSWORD: "Passwort entfernen?"
STR_PRESS_OK_SCAN: "OK für neue Suche"
STR_PRESS_ANY_CONTINUE: "Beliebige Taste drücken"
STR_SELECT_HINT: "links/rechts: Auswahl | OK: Best"
STR_HOW_CONNECT: "Wie möchtest du dich verbinden?"
STR_JOIN_NETWORK: "Netzwerk beitreten"
STR_CREATE_HOTSPOT: "Hotspot erstellen"
STR_JOIN_DESC: "Mit einem bestehenden WLAN verbinden"
STR_HOTSPOT_DESC: "WLAN für andere erstellen"
STR_STARTING_HOTSPOT: "Hotspot starten…"
STR_HOTSPOT_MODE: "Hotspot-Modus"
STR_CONNECT_WIFI_HINT: "Gerät mit diesem WLAN verbinden"
STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
STR_OR_HTTP_PREFIX: "oder http://"
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre-Web-URL"
STR_CONNECT_WIRELESS: "Als Drahtlos-Gerät hinzufügen"
STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
STR_MAC_ADDRESS: "MAC-Adresse:"
STR_CHECKING_WIFI: "WLAN prüfen…"
STR_ENTER_WIFI_PASSWORD: "WLAN-Passwort eingeben"
STR_ENTER_TEXT: "Text eingeben"
STR_TO_PREFIX: "bis"
STR_CALIBRE_DISCOVERING: "Calibre finden..."
STR_CALIBRE_CONNECTING_TO: "Verbinden mit"
STR_CALIBRE_CONNECTED_TO: "Verbunden mit"
STR_CALIBRE_WAITING_COMMANDS: "Auf Befehle warten…"
STR_CONNECTION_FAILED_RETRYING: "(Keine Verbindung, wiederholen)"
STR_CALIBRE_DISCONNECTED: "Calibre getrennt"
STR_CALIBRE_WAITING_TRANSFER: "Auf Übertragung warten..."
STR_CALIBRE_TRANSFER_HINT: "Bei Übertragungsfehler \\n'Freien Speicher ign.' in den\\nCalibre-Einstellungen einschalten."
STR_CALIBRE_RECEIVING: "Empfange:"
STR_CALIBRE_RECEIVED: "Empfangen:"
STR_CALIBRE_WAITING_MORE: "Auf mehr warten…"
STR_CALIBRE_FAILED_CREATE_FILE: "Speicherfehler"
STR_CALIBRE_PASSWORD_REQUIRED: "Passwort nötig"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Übertragung unterbrochen"
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader-Plugin installieren"
STR_CALIBRE_INSTRUCTION_2: "2) Mit selbem WLAN verbinden"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"An Gerät senden\""
STR_CALIBRE_INSTRUCTION_4: "Bildschirm beim Senden offenlassen"
STR_CAT_DISPLAY: "Anzeige"
STR_CAT_READER: "Lesen"
STR_CAT_CONTROLS: "Bedienung"
STR_CAT_SYSTEM: "System"
STR_CAT_CLOCK: "Uhr"
STR_SLEEP_SCREEN: "Standby-Bild"
STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
STR_STATUS_BAR: "Statusleiste"
STR_HIDE_BATTERY: "Batterie % ausblenden"
STR_EXTRA_SPACING: "Absatzabstand"
STR_TEXT_AA: "Schriftglättung"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung"
STR_FRONT_BTN_LAYOUT: "Vorderes Tastenlayout"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_LONG_PRESS_SKIP: "Langes Drücken springt Kap."
STR_FONT_FAMILY: "Lese-Schriftfamilie"
STR_EXT_READER_FONT: "Externe Schriftart"
STR_EXT_CHINESE_FONT: "Lese-Schriftart"
STR_EXT_UI_FONT: "Menü-Schriftart"
STR_FONT_SIZE: "Schriftgröße"
STR_LINE_SPACING: "Lese-Zeilenabstand"
STR_ASCII_LETTER_SPACING: "ASCII-Zeichenabstand"
STR_ASCII_DIGIT_SPACING: "ASCII-Ziffernabstand"
STR_CJK_SPACING: "CJK-Zeichenabstand"
STR_COLOR_MODE: "Farbmodus"
STR_SCREEN_MARGIN: "Lese-Seitenränder"
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
STR_HYPHENATION: "Silbentrennung"
STR_TIME_TO_SLEEP: "Standby nach"
STR_REFRESH_FREQ: "Anti-Ghosting nach"
STR_CALIBRE_SETTINGS: "Calibre-Einstellungen"
STR_KOREADER_SYNC: "KOReader-Synchr."
STR_CHECK_UPDATES: "Nach Updates suchen"
STR_LANGUAGE: "Sprache"
STR_SELECT_WALLPAPER: "Bildauswahl Standby"
STR_CLEAR_READING_CACHE: "Lese-Cache leeren"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Benutzername"
STR_PASSWORD: "Passwort nötig"
STR_SYNC_SERVER_URL: "Sync-Server-URL"
STR_DOCUMENT_MATCHING: "Dateizuordnung"
STR_AUTHENTICATE: "Authentifizieren"
STR_KOREADER_USERNAME: "KOReader-Benutzername"
STR_KOREADER_PASSWORD: "KOReader-Passwort"
STR_FILENAME: "Dateiname"
STR_BINARY: "Binärdatei"
STR_SET_CREDENTIALS_FIRST: "Zuerst anmelden"
STR_WIFI_CONN_FAILED: "WLAN-Verbindung fehlgeschlagen"
STR_AUTHENTICATING: "Authentifizieren…"
STR_AUTH_SUCCESS: "Erfolgreich authentifiziert!"
STR_KOREADER_AUTH: "KOReader-Auth"
STR_SYNC_READY: "KOReader-Synchronisierung bereit"
STR_AUTH_FAILED: "Authentifizierung fehlg."
STR_DONE: "Erledigt"
STR_CLEAR_CACHE_WARNING_1: "Alle Buch-Caches werden geleert."
STR_CLEAR_CACHE_WARNING_2: "Lesefortschritt wird gelöscht!"
STR_CLEAR_CACHE_WARNING_3: "Bücher müssen beim Öffnen"
STR_CLEAR_CACHE_WARNING_4: "neu eingelesen werden."
STR_CLEARING_CACHE: "Cache leeren…"
STR_CACHE_CLEARED: "Cache geleert"
STR_ITEMS_REMOVED: "Einträge entfernt"
STR_FAILED_LOWER: "fehlgeschlagen"
STR_CLEAR_CACHE_FAILED: "Fehler beim Cache-Leeren"
STR_CHECK_SERIAL_OUTPUT: "Serielle Ausgabe prüfen"
STR_DARK: "Dunkel"
STR_LIGHT: "Hell"
STR_CUSTOM: "Eigenes"
STR_COVER: "Umschlag"
STR_NONE_OPT: "Leer"
STR_FIT: "Anpassen"
STR_CROP: "Zuschnitt"
STR_NO_PROGRESS: "Ohne Fortschr."
STR_FULL_OPT: "Vollst."
STR_NEVER: "Nie"
STR_IN_READER: "Beim Lesen"
STR_ALWAYS: "Immer"
STR_IGNORE: "Ignorieren"
STR_SLEEP: "Standby"
STR_PAGE_TURN: "Umblättern"
STR_PORTRAIT: "Hochformat"
STR_LANDSCAPE_CW: "Querformat rechts"
STR_INVERTED: "Invertiert"
STR_LANDSCAPE_CCW: "Querformat links"
STR_FRONT_LAYOUT_BCLR: "Zurück, Bst, L, R"
STR_FRONT_LAYOUT_LRBC: "L, R, Zurück, Bst"
STR_FRONT_LAYOUT_LBCR: "L, Zurück, Bst, R"
STR_PREV_NEXT: "Zurück/Weiter"
STR_NEXT_PREV: "Weiter/Zuürck"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Klein"
STR_MEDIUM: "Mittel"
STR_LARGE: "Groß"
STR_X_LARGE: "Extragroß"
STR_TIGHT: "Eng"
STR_NORMAL: "Normal"
STR_WIDE: "Breit"
STR_JUSTIFY: "Blocksatz"
STR_ALIGN_LEFT: "Links"
STR_CENTER: "Zentriert"
STR_ALIGN_RIGHT: "Rechts"
STR_MIN_1: "1 Min"
STR_MIN_5: "5 Min"
STR_MIN_10: "10 Min"
STR_MIN_15: "15 Min"
STR_MIN_30: "30 Min"
STR_PAGES_1: "1 Seite"
STR_PAGES_5: "5 Seiten"
STR_PAGES_10: "10 Seiten"
STR_PAGES_15: "15 Seiten"
STR_PAGES_30: "30 Seiten"
STR_UPDATE: "Update"
STR_CHECKING_UPDATE: "Update suchen…"
STR_NEW_UPDATE: "Neues Update verfügbar!"
STR_CURRENT_VERSION: "Aktuelle Version:"
STR_NEW_VERSION: "Neue Version:"
STR_UPDATING: "Aktualisiere…"
STR_NO_UPDATE: "Kein Update verfügbar"
STR_UPDATE_FAILED: "Updatefehler"
STR_UPDATE_COMPLETE: "Update fertig"
STR_POWER_ON_HINT: "An-Knopf lang drücken, um neuzustarten"
STR_EXTERNAL_FONT: "Externe Schrift"
STR_BUILTIN_DISABLED: "Vorinstalliert (aus)"
STR_NO_ENTRIES: "Keine Einträge"
STR_DOWNLOADING: "Herunterladen…"
STR_DOWNLOAD_FAILED: "Ladefehler"
STR_ERROR_MSG: "Fehler:"
STR_UNNAMED: "Unbenannt"
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
STR_FETCH_FEED_FAILED: "Feedfehler"
STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
STR_NETWORK_PREFIX: "Netzwerk:"
STR_IP_ADDRESS_PREFIX: "IP-Adresse:"
STR_SCAN_QR_WIFI_HINT: "oder QR-Code mit dem Handy scannen für WLAN."
STR_ERROR_GENERAL_FAILURE: "Fehler: Allgemeiner Fehler"
STR_ERROR_NETWORK_NOT_FOUND: "Fehler: Kein Netzwerk"
STR_ERROR_CONNECTION_TIMEOUT: "Fehler: Zeitüberschreitung"
STR_SD_CARD: "SD-Karte"
STR_BACK: "« Zurück"
STR_EXIT: "« Verlassen"
STR_HOME: "« Start"
STR_SAVE: "« Speichern"
STR_SELECT: "Auswahl"
STR_TOGGLE: "Ändern"
STR_CONFIRM: "Bestätigen"
STR_CANCEL: "Abbrechen"
STR_CONNECT: "Verbinden"
STR_OPEN: "Öffnen"
STR_DOWNLOAD: "Herunterladen"
STR_RETRY: "Wiederh."
STR_YES: "Ja"
STR_NO: "Nein"
STR_STATE_ON: "An"
STR_STATE_OFF: "Aus"
STR_SET: "Gesetzt"
STR_NOT_SET: "Leer"
STR_DIR_LEFT: "Links"
STR_DIR_RIGHT: "Rechts"
STR_DIR_UP: "Hoch"
STR_DIR_DOWN: "Runter"
STR_CAPS_ON: "UMSCH"
STR_CAPS_OFF: "umsch"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[AN]"
STR_SLEEP_COVER_FILTER: "Standby-Coverfilter"
STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Komplett + Prozent"
STR_STATUS_BAR_FULL_BOOK: "Komplett + Buch"
STR_STATUS_BAR_BOOK_ONLY: "Nur Buch"
STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel"
STR_UI_THEME: "System-Design"
STR_THEME_CLASSIC: "Klassisch"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_OPDS_BROWSER: "OPDS-Browser"
STR_COVER_CUSTOM: "Umschlag + Eigenes"
STR_RECENTS: "Zuletzt"
STR_MENU_RECENT_BOOKS: "Zuletzt gelesen"
STR_NO_RECENT_BOOKS: "Keine Bücher"
STR_CALIBRE_DESC: "Calibre-Übertragung (WLAN)"
STR_FORGET_AND_REMOVE: "WLAN entfernen & Passwort löschen?"
STR_FORGET_BUTTON: "WLAN entfernen"
STR_CALIBRE_STARTING: "Calibre starten…"
STR_CALIBRE_SETUP: "Installation"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Leeren"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Entsprechende Vordertaste drücken"
STR_UNASSIGNED: "Leer"
STR_ALREADY_ASSIGNED: "Bereits zugeordnet"
STR_REMAP_RESET_HINT: "Seitentaste hoch: Standard"
STR_REMAP_CANCEL_HINT: "Seitentaste runter: Abbrechen"
STR_HW_BACK_LABEL: "Zurück (1. Taste)"
STR_HW_CONFIRM_LABEL: "Bestätigen (2. Taste)"
STR_HW_LEFT_LABEL: "Links (3. Taste)"
STR_HW_RIGHT_LABEL: "Rechts (4. Taste)"
STR_GO_TO_PERCENT: "Gehe zu %"
STR_GO_HOME_BUTTON: "Zum Anfang"
STR_SYNC_PROGRESS: "Fortschritt synchronisieren"
STR_DELETE_CACHE: "Buch-Cache leeren"
STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " Seiten | "
STR_BOOK_PREFIX: "Buch: "
STR_KBD_SHIFT: "umsch"
STR_KBD_SHIFT_CAPS: "UMSCH"
STR_KBD_LOCK: "FESTST"
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
STR_SYNCING_TIME: "Zeit synchonisieren…"
STR_CALC_HASH: "Dokument-Hash berechnen…"
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
STR_FETCH_PROGRESS: "Externen Fortschritt abrufen..."
STR_UPLOAD_PROGRESS: "Fortschritt hochladen…"
STR_NO_CREDENTIALS_MSG: "Zugangsdaten fehlen"
STR_KOREADER_SETUP_HINT: "KOReader-Konto unter Einst. anlegen"
STR_PROGRESS_FOUND: "Gefunden!"
STR_REMOTE_LABEL: "Extern:"
STR_LOCAL_LABEL: "Lokal:"
STR_PAGE_OVERALL_FORMAT: " Seite %d, %.2f%% insgesamt"
STR_PAGE_TOTAL_OVERALL_FORMAT: " Seite %d/%d, %.2f%% insgesamt"
STR_DEVICE_FROM_FORMAT: " Von: %s"
STR_APPLY_REMOTE: "Ext. Fortschritt übern."
STR_UPLOAD_LOCAL: "Lokalen Fortschritt hochl."
STR_NO_REMOTE_MSG: "Kein externer Fortschritt"
STR_UPLOAD_PROMPT: "Aktuelle Position hochladen?"
STR_UPLOAD_SUCCESS: "Hochgeladen!"
STR_SYNC_FAILED_MSG: "Fehlgeschlagen"
STR_SECTION_PREFIX: "Abschnitt"
STR_UPLOAD: "Hochladen"
STR_BOOK_S_STYLE: "Buch-Stil"
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
STR_OPDS_SERVER_URL: "OPDS-Server-URL"
STR_CHOOSE_SOMETHING: "Wähle etwas zum Lesen"
STR_CLOCK: "Uhr"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 Stunden"
STR_SET_TIME: "Uhrzeit einstellen"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,337 @@
_language_name: "Português (Brasil)"
_language_code: "PORTUGUESE"
_order: "5"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "INICIANDO"
STR_SLEEPING: "EM REPOUSO"
STR_ENTERING_SLEEP: "Entrando em repouso..."
STR_BROWSE_FILES: "Arquivos"
STR_FILE_TRANSFER: "Transferência"
STR_SETTINGS_TITLE: "Configurações"
STR_CALIBRE_LIBRARY: "Biblioteca do Calibre"
STR_CONTINUE_READING: "Continuar lendo"
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
STR_START_READING: "Comece a ler abaixo"
STR_BOOKS: "Livros"
STR_NO_BOOKS_FOUND: "Nenhum livro encontrado"
STR_SELECT_CHAPTER: "Escolher capítulo"
STR_NO_CHAPTERS: "Sem capítulos"
STR_END_OF_BOOK: "Fim do livro"
STR_EMPTY_CHAPTER: "Capítulo vazio"
STR_INDEXING: "Indexando..."
STR_MEMORY_ERROR: "Erro de memória"
STR_PAGE_LOAD_ERROR: "Erro página"
STR_EMPTY_FILE: "Arquivo vazio"
STR_OUT_OF_BOUNDS: "Fora dos limites"
STR_LOADING: "Carregando..."
STR_LOAD_XTC_FAILED: "Falha ao carregar XTC"
STR_LOAD_TXT_FAILED: "Falha ao carregar TXT"
STR_LOAD_EPUB_FAILED: "Falha ao carregar EPUB"
STR_SD_CARD_ERROR: "Erro no cartão SD"
STR_WIFI_NETWORKS: "Redes WiFi"
STR_NO_NETWORKS: "Sem redes"
STR_NETWORKS_FOUND: "%zu redes encontradas"
STR_SCANNING: "Procurando..."
STR_CONNECTING: "Conectando..."
STR_CONNECTED: "Conectado!"
STR_CONNECTION_FAILED: "Falha na conexão"
STR_CONNECTION_TIMEOUT: "Tempo limite conexão"
STR_FORGET_NETWORK: "Esquecer rede?"
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
STR_REMOVE_PASSWORD: "Remover senha salva?"
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
STR_PRESS_ANY_CONTINUE: "Pressione qualquer botão continuar"
STR_SELECT_HINT: "ESQ/DIR: Escolher | OK: Confirmar"
STR_HOW_CONNECT: "Como você gostaria se conectar?"
STR_JOIN_NETWORK: "Entrar em uma rede"
STR_CREATE_HOTSPOT: "Criar hotspot"
STR_JOIN_DESC: "Conecte-se a uma rede WiFi existente"
STR_HOTSPOT_DESC: "Crie uma rede WiFi outras pessoas entrarem"
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
STR_HOTSPOT_MODE: "Modo hotspot"
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede WiFi"
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
STR_CALIBRE_WIRELESS: "Calibre sem fio"
STR_CALIBRE_WEB_URL: "URL do Calibre Web"
STR_CONNECT_WIRELESS: "Conectar como dispositivo sem fio"
STR_NETWORK_LEGEND: "* = Criptografada | + = Salva"
STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "Verificando WiFi..."
STR_ENTER_WIFI_PASSWORD: "Digite a senha WiFi"
STR_ENTER_TEXT: "Inserir texto"
STR_TO_PREFIX: "para"
STR_CALIBRE_DISCOVERING: "Procurando o Calibre..."
STR_CALIBRE_CONNECTING_TO: "Conectando a"
STR_CALIBRE_CONNECTED_TO: "Conectado a"
STR_CALIBRE_WAITING_COMMANDS: "Aguardando comandos..."
STR_CONNECTION_FAILED_RETRYING: "(Falha conexão, tentando novamente)"
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
STR_CALIBRE_WAITING_TRANSFER: "Aguardando transferência..."
STR_CALIBRE_TRANSFER_HINT: "Se a transferência falhar, ative\n\\n'Ignorar espaço livre'\\n nas \\nconfigurações do\nplugin SmartDevice\\n Calibre."
STR_CALIBRE_RECEIVING: "Recebendo:"
STR_CALIBRE_RECEIVED: "Recebido:"
STR_CALIBRE_WAITING_MORE: "Aguardando mais..."
STR_CALIBRE_FAILED_CREATE_FILE: "Falha ao criar o arquivo"
STR_CALIBRE_PASSWORD_REQUIRED: "Senha obrigatória"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transf. interrompida"
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja mesma rede WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
STR_CAT_DISPLAY: "Tela"
STR_CAT_READER: "Leitor"
STR_CAT_CONTROLS: "Controles"
STR_CAT_SYSTEM: "Sistema"
STR_CAT_CLOCK: "Relógio"
STR_SLEEP_SCREEN: "Tela de repouso"
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
STR_STATUS_BAR: "Barra de status"
STR_HIDE_BATTERY: "Ocultar % da bateria"
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
STR_TEXT_AA: "Suavização de texto"
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
STR_ORIENTATION: "Orientação de leitura"
STR_FRONT_BTN_LAYOUT: "Disposição botões frontais"
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
STR_LONG_PRESS_SKIP: "Pular capítulo com pressão longa"
STR_FONT_FAMILY: "Fonte do leitor"
STR_EXT_READER_FONT: "Fonte leitor externo"
STR_EXT_CHINESE_FONT: "Fonte do leitor"
STR_EXT_UI_FONT: "Fonte da interface"
STR_FONT_SIZE: "Tam. fonte UI"
STR_LINE_SPACING: "Espaçamento entre linhas"
STR_ASCII_LETTER_SPACING: "Espaçamento letras ASCII"
STR_ASCII_DIGIT_SPACING: "Espaçamento dígitos ASCII"
STR_CJK_SPACING: "Espaçamento CJK"
STR_COLOR_MODE: "Modo de cor"
STR_SCREEN_MARGIN: "Margens da tela"
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
STR_HYPHENATION: "Hifenização"
STR_TIME_TO_SLEEP: "Tempo para repousar"
STR_REFRESH_FREQ: "Frequência atualização"
STR_CALIBRE_SETTINGS: "Configuração do Calibre"
STR_KOREADER_SYNC: "Sincronização KOReader"
STR_CHECK_UPDATES: "Verificar atualizações"
STR_LANGUAGE: "Idioma"
STR_SELECT_WALLPAPER: "Escolher papel parede"
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nome de usuário"
STR_PASSWORD: "Senha"
STR_SYNC_SERVER_URL: "URL servidor sincronização"
STR_DOCUMENT_MATCHING: "Documento correspondente"
STR_AUTHENTICATE: "Autenticar"
STR_KOREADER_USERNAME: "Usuário do KOReader"
STR_KOREADER_PASSWORD: "Senha do KOReader"
STR_FILENAME: "Nome do arquivo"
STR_BINARY: "Binário"
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
STR_WIFI_CONN_FAILED: "Falha na conexão WiFi"
STR_AUTHENTICATING: "Autenticando..."
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
STR_KOREADER_AUTH: "Autenticação KOReader"
STR_SYNC_READY: "A sincronização KOReader está pronta uso"
STR_AUTH_FAILED: "Falha na autenticação"
STR_DONE: "Feito"
STR_CLEAR_CACHE_WARNING_1: "Isso vai limpar todos os dados livros em cache."
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
STR_CLEAR_CACHE_WARNING_3: "Os livros precisarão ser reindexados"
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
STR_CLEARING_CACHE: "Limpando cache..."
STR_CACHE_CLEARED: "Cache limpo"
STR_ITEMS_REMOVED: "itens removidos"
STR_FAILED_LOWER: "falhou"
STR_CLEAR_CACHE_FAILED: "Falha ao limpar o cache"
STR_CHECK_SERIAL_OUTPUT: "Ver saída serial"
STR_DARK: "Escuro"
STR_LIGHT: "Claro"
STR_CUSTOM: "Personalizado"
STR_COVER: "Capa"
STR_NONE_OPT: "Nenhum"
STR_FIT: "Ajustar"
STR_CROP: "Recortar"
STR_NO_PROGRESS: "Sem progresso"
STR_FULL_OPT: "Completo"
STR_NEVER: "Nunca"
STR_IN_READER: "No leitor"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página"
STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Paisagem AH"
STR_FRONT_LAYOUT_BCLR: "Vol, Conf, Esq, Dir"
STR_FRONT_LAYOUT_LRBC: "Esq, Dir, Vol, Conf"
STR_FRONT_LAYOUT_LBCR: "Esq, Vol, Conf, Dir"
STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Pequeno"
STR_MEDIUM: "Médio"
STR_LARGE: "Grande"
STR_X_LARGE: "Extra grande"
STR_TIGHT: "Apertado"
STR_NORMAL: "Normal"
STR_WIDE: "Largo"
STR_JUSTIFY: "Justificar"
STR_ALIGN_LEFT: "Esquerda"
STR_CENTER: "Centralizar"
STR_ALIGN_RIGHT: "Direita"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 página"
STR_PAGES_5: "5 páginas"
STR_PAGES_10: "10 páginas"
STR_PAGES_15: "15 páginas"
STR_PAGES_30: "30 páginas"
STR_UPDATE: "Atualizar"
STR_CHECKING_UPDATE: "Verificando atualização..."
STR_NEW_UPDATE: "Nova atualização disponível!"
STR_CURRENT_VERSION: "Versão atual:"
STR_NEW_VERSION: "Nova versão:"
STR_UPDATING: "Atualizando..."
STR_NO_UPDATE: "Nenhuma atualização disponível"
STR_UPDATE_FAILED: "Falha na atualização"
STR_UPDATE_COMPLETE: "Atualização concluída"
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente"
STR_EXTERNAL_FONT: "Fonte externa"
STR_BUILTIN_DISABLED: "Integrada (desativada)"
STR_NO_ENTRIES: "Nenhum entries encontrado"
STR_DOWNLOADING: "Baixando..."
STR_DOWNLOAD_FAILED: "Falha no download"
STR_ERROR_MSG: "Erro:"
STR_UNNAMED: "Sem nome"
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
STR_NETWORK_PREFIX: "Rede:"
STR_IP_ADDRESS_PREFIX: "Endereço IP:"
STR_SCAN_QR_WIFI_HINT: "ou escaneie o QR code com seu celular conectar ao WiFi."
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
STR_SD_CARD: "Cartão SD"
STR_BACK: "« Voltar"
STR_EXIT: "« Sair"
STR_HOME: "« Início"
STR_SAVE: "« Salvar"
STR_SELECT: "Escolher"
STR_TOGGLE: "Alternar"
STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar"
STR_OPEN: "Abrir"
STR_DOWNLOAD: "Baixar"
STR_RETRY: "Tentar novamente"
STR_YES: "Sim"
STR_NO: "Não"
STR_STATE_ON: "LIG."
STR_STATE_OFF: "DESL."
STR_SET: "Definir"
STR_NOT_SET: "Não definido"
STR_DIR_LEFT: "Esquerda"
STR_DIR_RIGHT: "Direita"
STR_DIR_UP: "Cima"
STR_DIR_DOWN: "Baixo"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[LIGADO]"
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
STR_FILTER_CONTRAST: "Contraste"
STR_STATUS_BAR_FULL_PERCENT: "Completa c/ porcentagem"
STR_STATUS_BAR_FULL_BOOK: "Completa c/ barra livro"
STR_STATUS_BAR_BOOK_ONLY: "Só barra do livro"
STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo"
STR_UI_THEME: "Tema da interface"
STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Capa + personalizado"
STR_RECENTS: "Recentes"
STR_MENU_RECENT_BOOKS: "Livros recentes"
STR_NO_RECENT_BOOKS: "Sem livros recentes"
STR_CALIBRE_DESC: "Usar transferências sem fio Calibre"
STR_FORGET_AND_REMOVE: "Esquecer a rede e remover a senha salva?"
STR_FORGET_BUTTON: "Esquecer rede"
STR_CALIBRE_STARTING: "Iniciando Calibre..."
STR_CALIBRE_SETUP: "Configuração"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Limpar"
STR_DEFAULT_VALUE: "Padrão"
STR_REMAP_PROMPT: "Pressione um botão frontal cada função"
STR_UNASSIGNED: "Não atribuído"
STR_ALREADY_ASSIGNED: "Já atribuído"
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão"
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento"
STR_HW_BACK_LABEL: "Voltar (1º botão)"
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
STR_GO_TO_PERCENT: "Ir para %"
STR_GO_HOME_BUTTON: "Ir para o início"
STR_SYNC_PROGRESS: "Sincronizar progresso"
STR_DELETE_CACHE: "Excluir cache do livro"
STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: "páginas |"
STR_BOOK_PREFIX: "Livro:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "TRAVAR"
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_CALC_HASH: "Calculando hash documento..."
STR_HASH_FAILED: "Falha ao calcular o hash documento"
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
STR_UPLOAD_PROGRESS: "Enviando progresso..."
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader em Config."
STR_PROGRESS_FOUND: "Progresso encontrado!"
STR_REMOTE_LABEL: "Remoto:"
STR_LOCAL_LABEL: "Local:"
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
STR_DEVICE_FROM_FORMAT: "De: %s"
STR_APPLY_REMOTE: "Aplicar progresso remoto"
STR_UPLOAD_LOCAL: "Enviar progresso local"
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
STR_UPLOAD_PROMPT: "Enviar posição atual?"
STR_UPLOAD_SUCCESS: "Progresso enviado!"
STR_SYNC_FAILED_MSG: "Falha na sincronização"
STR_SECTION_PREFIX: "Seção"
STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido"
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_CHOOSE_SOMETHING: "Escolha algo para ler"
STR_CLOCK: "Relógio"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 horas"
STR_SET_TIME: "Definir hora"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,337 @@
_language_name: "Русский"
_language_code: "RUSSIAN"
_order: "6"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "Загрузка"
STR_SLEEPING: "Спящий режим"
STR_ENTERING_SLEEP: "Переход в сон..."
STR_BROWSE_FILES: "Обзор файлов"
STR_FILE_TRANSFER: "Передача файлов"
STR_SETTINGS_TITLE: "Настройки"
STR_CALIBRE_LIBRARY: "Библиотека Calibre"
STR_CONTINUE_READING: "Продолжить чтение"
STR_NO_OPEN_BOOK: "Нет открытой книги"
STR_START_READING: "Начать чтение ниже"
STR_BOOKS: "Книги"
STR_NO_BOOKS_FOUND: "Книги не найдены"
STR_SELECT_CHAPTER: "Выберите главу"
STR_NO_CHAPTERS: "Глав нет"
STR_END_OF_BOOK: "Конец книги"
STR_EMPTY_CHAPTER: "Пустая глава"
STR_INDEXING: "Индексация..."
STR_MEMORY_ERROR: "Ошибка памяти"
STR_PAGE_LOAD_ERROR: "Ошибка загрузки страницы"
STR_EMPTY_FILE: "Пустой файл"
STR_OUT_OF_BOUNDS: "Выход за пределы"
STR_LOADING: "Загрузка..."
STR_LOAD_XTC_FAILED: "Не удалось загрузить XTC"
STR_LOAD_TXT_FAILED: "Не удалось загрузить TXT"
STR_LOAD_EPUB_FAILED: "Не удалось загрузить EPUB"
STR_SD_CARD_ERROR: "Ошибка SD-карты"
STR_WIFI_NETWORKS: "Wi-Fi сети"
STR_NO_NETWORKS: "Сети не найдены"
STR_NETWORKS_FOUND: "Найдено сетей: %zu"
STR_SCANNING: "Сканирование..."
STR_CONNECTING: "Подключение..."
STR_CONNECTED: "Подключено!"
STR_CONNECTION_FAILED: "Ошибка подключения"
STR_CONNECTION_TIMEOUT: "Тайм-аут подключения"
STR_FORGET_NETWORK: "Забыть сеть?"
STR_SAVE_PASSWORD: "Сохранить пароль?"
STR_REMOVE_PASSWORD: "Удалить сохранённый пароль?"
STR_PRESS_OK_SCAN: "Нажмите OK для повторного поиска"
STR_PRESS_ANY_CONTINUE: "Нажмите любую кнопку"
STR_SELECT_HINT: "ВЛЕВО/ВПРАВО: выбор | OK: подтвердить"
STR_HOW_CONNECT: "Как вы хотите подключиться?"
STR_JOIN_NETWORK: "Подключиться к сети"
STR_CREATE_HOTSPOT: "Создать точку доступа"
STR_JOIN_DESC: "Подключение к существующей сети Wi-Fi"
STR_HOTSPOT_DESC: "Создать сеть Wi-Fi для подключения других"
STR_STARTING_HOTSPOT: "Запуск точки доступа..."
STR_HOTSPOT_MODE: "Режим точки доступа"
STR_CONNECT_WIFI_HINT: "Подключите устройство к этой сети Wi-Fi"
STR_OPEN_URL_HINT: "Откройте этот адрес в браузере"
STR_OR_HTTP_PREFIX: "или http://"
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
STR_CONNECT_WIRELESS: "Подключить как беспроводное устройство"
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
STR_MAC_ADDRESS: "MAC-адрес:"
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Введите пароль Wi-Fi"
STR_ENTER_TEXT: "Введите текст"
STR_TO_PREFIX: "к "
STR_CALIBRE_DISCOVERING: "Поиск Calibre..."
STR_CALIBRE_CONNECTING_TO: "Подключение к "
STR_CALIBRE_CONNECTED_TO: "Подключено к "
STR_CALIBRE_WAITING_COMMANDS: "Ожидание команд..."
STR_CONNECTION_FAILED_RETRYING: "(Ошибка подключения"
STR_CALIBRE_DISCONNECTED: "Соединение с Calibre разорвано"
STR_CALIBRE_WAITING_TRANSFER: "Ожидание передачи..."
STR_CALIBRE_TRANSFER_HINT: "Если передача не удаётся"
STR_CALIBRE_RECEIVING: "Получение:"
STR_CALIBRE_RECEIVED: "Получено:"
STR_CALIBRE_WAITING_MORE: "Ожидание следующих файлов..."
STR_CALIBRE_FAILED_CREATE_FILE: "Не удалось создать файл"
STR_CALIBRE_PASSWORD_REQUIRED: "Требуется пароль"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Передача прервана"
STR_CALIBRE_INSTRUCTION_1: "1) Установите плагин CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Подключитесь к той же сети Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) В Calibre выберите: «Отправить на устройство»"
STR_CALIBRE_INSTRUCTION_4: "Не закрывайте этот экран во время отправки"
STR_CAT_DISPLAY: "Экран"
STR_CAT_READER: "Чтение"
STR_CAT_CONTROLS: "Управление"
STR_CAT_SYSTEM: "Система"
STR_CAT_CLOCK: "Часы"
STR_SLEEP_SCREEN: "Экран сна"
STR_SLEEP_COVER_MODE: "Режим обложки сна"
STR_STATUS_BAR: "Строка состояния"
STR_HIDE_BATTERY: "Скрыть % батареи"
STR_EXTRA_SPACING: "Доп. интервал абзаца"
STR_TEXT_AA: "Сглаживание текста"
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
STR_ORIENTATION: "Ориентация чтения"
STR_FRONT_BTN_LAYOUT: "Боковые кнопки"
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
STR_LONG_PRESS_SKIP: "Долгое нажатие - смена главы"
STR_FONT_FAMILY: "Шрифт чтения"
STR_EXT_READER_FONT: "Внешний шрифт чтения"
STR_EXT_CHINESE_FONT: "Шрифт CJK"
STR_EXT_UI_FONT: "Шрифт интерфейса"
STR_FONT_SIZE: "Размер шрифта интерфейса"
STR_LINE_SPACING: "Межстрочный интервал"
STR_ASCII_LETTER_SPACING: "Интервал букв ASCII"
STR_ASCII_DIGIT_SPACING: "Интервал цифр ASCII"
STR_CJK_SPACING: "Интервал CJK"
STR_COLOR_MODE: "Цветовой режим"
STR_SCREEN_MARGIN: "Поля экрана"
STR_PARA_ALIGNMENT: "Выравнивание абзаца"
STR_HYPHENATION: "Перенос слов"
STR_TIME_TO_SLEEP: "Сон через"
STR_REFRESH_FREQ: "Частота обновления"
STR_CALIBRE_SETTINGS: "Настройки Calibre"
STR_KOREADER_SYNC: "Синхронизация KOReader"
STR_CHECK_UPDATES: "Проверить обновления"
STR_LANGUAGE: "Язык"
STR_SELECT_WALLPAPER: "Выбрать обои"
STR_CLEAR_READING_CACHE: "Очистить кэш чтения"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Имя пользователя"
STR_PASSWORD: "Пароль"
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
STR_DOCUMENT_MATCHING: "Сопоставление документов"
STR_AUTHENTICATE: "Авторизация"
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
STR_KOREADER_PASSWORD: "Пароль KOReader"
STR_FILENAME: "Имя файла"
STR_BINARY: "Бинарный"
STR_SET_CREDENTIALS_FIRST: "Сначала укажите данные"
STR_WIFI_CONN_FAILED: "Не удалось подключиться к Wi-Fi"
STR_AUTHENTICATING: "Авторизация..."
STR_AUTH_SUCCESS: "Авторизация успешна!"
STR_KOREADER_AUTH: "Авторизация KOReader"
STR_SYNC_READY: "Синхронизация KOReader готова"
STR_AUTH_FAILED: "Ошибка авторизации"
STR_DONE: "Готово"
STR_CLEAR_CACHE_WARNING_1: "Будут удалены все данные кэша книг."
STR_CLEAR_CACHE_WARNING_2: "Весь прогресс чтения будет потерян!"
STR_CLEAR_CACHE_WARNING_3: "Книги потребуется переиндексировать"
STR_CLEAR_CACHE_WARNING_4: "при повторном открытии."
STR_CLEARING_CACHE: "Очистка кэша..."
STR_CACHE_CLEARED: "Кэш очищен"
STR_ITEMS_REMOVED: "элементов удалено"
STR_FAILED_LOWER: "ошибка"
STR_CLEAR_CACHE_FAILED: "Не удалось очистить кэш"
STR_CHECK_SERIAL_OUTPUT: "Проверьте вывод по UART для деталей"
STR_DARK: "Тёмный"
STR_LIGHT: "Светлый"
STR_CUSTOM: "Свой"
STR_COVER: "Обложка"
STR_NONE_OPT: "Нет"
STR_FIT: "Вписать"
STR_CROP: "Обрезать"
STR_NO_PROGRESS: "Без прогресса"
STR_FULL_OPT: "Полная"
STR_NEVER: "Никогда"
STR_IN_READER: "В режиме чтения"
STR_ALWAYS: "Всегда"
STR_IGNORE: "Игнорировать"
STR_SLEEP: "Сон"
STR_PAGE_TURN: "Перелистывание"
STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Инверсия"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_FRONT_LAYOUT_BCLR: "Наз, Ок, Лев, Прав"
STR_FRONT_LAYOUT_LRBC: "Лев, Прав, Наз, Ок"
STR_FRONT_LAYOUT_LBCR: "Лев, Наз, Ок, Прав"
STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Маленький"
STR_MEDIUM: "Средний"
STR_LARGE: "Большой"
STR_X_LARGE: "Очень большой"
STR_TIGHT: "Узкий"
STR_NORMAL: "Обычный"
STR_WIDE: "Широкий"
STR_JUSTIFY: "По ширине"
STR_ALIGN_LEFT: "По левому краю"
STR_CENTER: "По центру"
STR_ALIGN_RIGHT: "По правому краю"
STR_MIN_1: "1 мин"
STR_MIN_5: "5 мин"
STR_MIN_10: "10 мин"
STR_MIN_15: "15 мин"
STR_MIN_30: "30 мин"
STR_PAGES_1: "1 стр."
STR_PAGES_5: "5 стр."
STR_PAGES_10: "10 стр."
STR_PAGES_15: "15 стр."
STR_PAGES_30: "30 стр."
STR_UPDATE: "Обновление"
STR_CHECKING_UPDATE: "Проверка обновлений..."
STR_NEW_UPDATE: "Доступно новое обновление!"
STR_CURRENT_VERSION: "Текущая версия:"
STR_NEW_VERSION: "Новая версия:"
STR_UPDATING: "Обновление..."
STR_NO_UPDATE: "Обновлений нет"
STR_UPDATE_FAILED: "Ошибка обновления"
STR_UPDATE_COMPLETE: "Обновление завершено"
STR_POWER_ON_HINT: "Удерживайте кнопку питания для включения"
STR_EXTERNAL_FONT: "Пользовательский шрифт"
STR_BUILTIN_DISABLED: "Встроенный (отключён)"
STR_NO_ENTRIES: "Записи не найдены"
STR_DOWNLOADING: "Загрузка..."
STR_DOWNLOAD_FAILED: "Ошибка загрузки"
STR_ERROR_MSG: "Ошибка:"
STR_UNNAMED: "Без имени"
STR_NO_SERVER_URL: "URL сервера не настроен"
STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
STR_NETWORK_PREFIX: "Сеть:"
STR_IP_ADDRESS_PREFIX: "IP-адрес:"
STR_SCAN_QR_WIFI_HINT: "или отсканируйте QR-код для подключения к Wi-Fi."
STR_ERROR_GENERAL_FAILURE: "Ошибка: Общая ошибка"
STR_ERROR_NETWORK_NOT_FOUND: "Ошибка: Сеть не найдена"
STR_ERROR_CONNECTION_TIMEOUT: "Ошибка: Тайм-аут соединения"
STR_SD_CARD: "SD-карта"
STR_BACK: "« Назад"
STR_EXIT: "« Выход"
STR_HOME: "« Главная"
STR_SAVE: "« Сохранить"
STR_SELECT: "Выбрать"
STR_TOGGLE: "Выбор"
STR_CONFIRM: "Подтв."
STR_CANCEL: "Отмена"
STR_CONNECT: "Подкл."
STR_OPEN: "Открыть"
STR_DOWNLOAD: "Скачать"
STR_RETRY: "Повторить"
STR_YES: "Да"
STR_NO: "Нет"
STR_STATE_ON: "ВКЛ"
STR_STATE_OFF: "ВЫКЛ"
STR_SET: "Установлено"
STR_NOT_SET: "Не установлено"
STR_DIR_LEFT: "Влево"
STR_DIR_RIGHT: "Вправо"
STR_DIR_UP: "Вверх"
STR_DIR_DOWN: "Вниз"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ВКЛ]"
STR_SLEEP_COVER_FILTER: "Фильтр экрана сна"
STR_FILTER_CONTRAST: "Контраст"
STR_STATUS_BAR_FULL_PERCENT: "Полная + %"
STR_STATUS_BAR_FULL_BOOK: "Полная + шкала книги"
STR_STATUS_BAR_BOOK_ONLY: "Только шкала книги"
STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы"
STR_UI_THEME: "Тема интерфейса"
STR_THEME_CLASSIC: "Классическая"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_OPDS_BROWSER: "OPDS браузер"
STR_COVER_CUSTOM: "Обложка + Свой"
STR_RECENTS: "Недавние"
STR_MENU_RECENT_BOOKS: "Недавние книги"
STR_NO_RECENT_BOOKS: "Нет недавних книг"
STR_CALIBRE_DESC: "Использовать беспроводную передачу Calibre"
STR_FORGET_AND_REMOVE: "Забыть сеть и удалить сохранённый пароль?"
STR_FORGET_BUTTON: "Забыть сеть"
STR_CALIBRE_STARTING: "Запуск Calibre..."
STR_CALIBRE_SETUP: "Настройка"
STR_CALIBRE_STATUS: "Статус"
STR_CLEAR_BUTTON: "Очистить"
STR_DEFAULT_VALUE: "По умолчанию"
STR_REMAP_PROMPT: "Назначьте роль для каждой кнопки"
STR_UNASSIGNED: "Не назначено"
STR_ALREADY_ASSIGNED: "Уже назначено"
STR_REMAP_RESET_HINT: "Боковая кнопка вверх: сбросить по умолчанию"
STR_REMAP_CANCEL_HINT: "Боковая кнопка вниз: отменить переназначение"
STR_HW_BACK_LABEL: "Назад (1-я кнопка)"
STR_HW_CONFIRM_LABEL: "Подтвердить (2-я кнопка)"
STR_HW_LEFT_LABEL: "Влево (3-я кнопка)"
STR_HW_RIGHT_LABEL: "Вправо (4-я кнопка)"
STR_GO_TO_PERCENT: "Перейти к %"
STR_GO_HOME_BUTTON: "На главную"
STR_SYNC_PROGRESS: "Синхронизировать прогресс"
STR_DELETE_CACHE: "Удалить кэш книги"
STR_CHAPTER_PREFIX: "Глава:"
STR_PAGES_SEPARATOR: "стр. |"
STR_BOOK_PREFIX: "Книга:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
STR_SYNCING_TIME: "Синхронизация времени..."
STR_CALC_HASH: "Расчёт хэша документа..."
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
STR_FETCH_PROGRESS: "Получение удалённого прогресса..."
STR_UPLOAD_PROGRESS: "Отправка прогресса..."
STR_NO_CREDENTIALS_MSG: "Данные для входа не настроены"
STR_KOREADER_SETUP_HINT: "Настройте аккаунт KOReader в настройках"
STR_PROGRESS_FOUND: "Прогресс найден!"
STR_REMOTE_LABEL: "Удалённый:"
STR_LOCAL_LABEL: "Локальный:"
STR_PAGE_OVERALL_FORMAT: "Страница %d, %.2f%% всего"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Страница %d/%d"
STR_DEVICE_FROM_FORMAT: "От: %s"
STR_APPLY_REMOTE: "Применить удалённый прогресс"
STR_UPLOAD_LOCAL: "Отправить локальный прогресс"
STR_NO_REMOTE_MSG: "Удалённый прогресс не найден"
STR_UPLOAD_PROMPT: "Отправить текущую позицию?"
STR_UPLOAD_SUCCESS: "Прогресс отправлен!"
STR_SYNC_FAILED_MSG: "Ошибка синхронизации"
STR_SECTION_PREFIX: "Раздел"
STR_UPLOAD: "Отправить"
STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Встроенный стиль"
STR_OPDS_SERVER_URL: "URL OPDS сервера"
STR_CHOOSE_SOMETHING: "Выберите что-нибудь для чтения"
STR_CLOCK: "Часы"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 часа"
STR_SET_TIME: "Установить время"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,337 @@
_language_name: "Español"
_language_code: "SPANISH"
_order: "1"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BOOTING"
STR_SLEEPING: "SLEEPING"
STR_ENTERING_SLEEP: "ENTERING SLEEP..."
STR_BROWSE_FILES: "Buscar archivos"
STR_FILE_TRANSFER: "Transferencia de archivos"
STR_SETTINGS_TITLE: "Configuración"
STR_CALIBRE_LIBRARY: "Libreria Calibre"
STR_CONTINUE_READING: "Continuar leyendo"
STR_NO_OPEN_BOOK: "No hay libros abiertos"
STR_START_READING: "Start reading below"
STR_BOOKS: "Libros"
STR_NO_BOOKS_FOUND: "No se encontraron libros"
STR_SELECT_CHAPTER: "Seleccionar capítulo"
STR_NO_CHAPTERS: "Sin capítulos"
STR_END_OF_BOOK: "Fin del libro"
STR_EMPTY_CHAPTER: "Capítulo vacío"
STR_INDEXING: "Indexando..."
STR_MEMORY_ERROR: "Error de memoria"
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
STR_EMPTY_FILE: "Archivo vacío"
STR_OUT_OF_BOUNDS: "Out of bounds"
STR_LOADING: "Cargando..."
STR_LOAD_XTC_FAILED: "Error al cargar XTC"
STR_LOAD_TXT_FAILED: "Error al cargar TXT"
STR_LOAD_EPUB_FAILED: "Error al cargar EPUB"
STR_SD_CARD_ERROR: "Error en la tarjeta SD"
STR_WIFI_NETWORKS: "Redes Wi-Fi"
STR_NO_NETWORKS: "No hay redes disponibles"
STR_NETWORKS_FOUND: "%zu redes encontradas"
STR_SCANNING: "Buscando..."
STR_CONNECTING: "Conectando..."
STR_CONNECTED: "Conectado!"
STR_CONNECTION_FAILED: "Error de conexion"
STR_CONNECTION_TIMEOUT: "Connection timeout"
STR_FORGET_NETWORK: "Olvidar la red?"
STR_SAVE_PASSWORD: "Guardar contraseña para la próxima vez?"
STR_REMOVE_PASSWORD: "Borrar contraseñas guardadas?"
STR_PRESS_OK_SCAN: "Presione OK para buscar de nuevo"
STR_PRESS_ANY_CONTINUE: "Presione cualquier botón para continuar"
STR_SELECT_HINT: "Izquierda/Derecha: Seleccionar | OK: Confirmar"
STR_HOW_CONNECT: "Cómo te gustaría conectarte?"
STR_JOIN_NETWORK: "Unirse a una red"
STR_CREATE_HOTSPOT: "Crear punto de acceso"
STR_JOIN_DESC: "Conectarse a una red Wi-Fi existente"
STR_HOTSPOT_DESC: "Crear una red Wi-Fi para que otros se unan"
STR_STARTING_HOTSPOT: "Iniciando punto de acceso..."
STR_HOTSPOT_MODE: "Modo punto de acceso"
STR_CONNECT_WIFI_HINT: "Conectar su dispositivo a esta red Wi-Fi"
STR_OPEN_URL_HINT: "Abre esta dirección en tu navegador"
STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanee este código QR con su móvil:"
STR_CALIBRE_WIRELESS: "Calibre inalámbrico"
STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre"
STR_CONNECT_WIRELESS: "Conectar como dispositivo inalámbrico"
STR_NETWORK_LEGEND: "* = Cifrado | + = Guardado"
STR_MAC_ADDRESS: "Dirección MAC:"
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Introduzca la contraseña de Wi-Fi"
STR_ENTER_TEXT: "Introduzca el texto"
STR_TO_PREFIX: "a "
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
STR_CALIBRE_CONNECTING_TO: "Conectándose a"
STR_CALIBRE_CONNECTED_TO: "Conectado a "
STR_CALIBRE_WAITING_COMMANDS: "Esperando comandos..."
STR_CONNECTION_FAILED_RETRYING: "(Error de conexión, intentándolo nuevamente)"
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
STR_CALIBRE_WAITING_TRANSFER: "Esperando transferencia..."
STR_CALIBRE_TRANSFER_HINT: "Si la transferencia falla, habilite \\n'Ignorar espacio libre' en las configuraciones del \\nplugin smartdevice de calibre."
STR_CALIBRE_RECEIVING: "Recibiendo: "
STR_CALIBRE_RECEIVED: "Recibido: "
STR_CALIBRE_WAITING_MORE: "Esperando más..."
STR_CALIBRE_FAILED_CREATE_FILE: "Error al crear el archivo"
STR_CALIBRE_PASSWORD_REQUIRED: "Contraseña requerida"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transferencia interrumpida"
STR_CALIBRE_INSTRUCTION_1: "1) Instala CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Conéctese a la misma red Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) En Calibre: \"Enviar a dispotivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Permanezca en esta pantalla mientras se envía\""
STR_CAT_DISPLAY: "Pantalla"
STR_CAT_READER: "Lector"
STR_CAT_CONTROLS: "Control"
STR_CAT_SYSTEM: "Sistema"
STR_CAT_CLOCK: "Reloj"
STR_SLEEP_SCREEN: "Salva Pantallas"
STR_SLEEP_COVER_MODE: "Modo de salva pantallas"
STR_STATUS_BAR: "Barra de estado"
STR_HIDE_BATTERY: "Ocultar porcentaje de batería"
STR_EXTRA_SPACING: "Espaciado extra de párrafos"
STR_TEXT_AA: "Suavizado de bordes de texto"
STR_SHORT_PWR_BTN: "Clic breve del botón de encendido"
STR_ORIENTATION: "Orientación de la lectura"
STR_FRONT_BTN_LAYOUT: "Diseño de los botones frontales"
STR_SIDE_BTN_LAYOUT: "Diseño de los botones laterales (Lector)"
STR_LONG_PRESS_SKIP: "Pasar a la capítulo al presiónar largamente"
STR_FONT_FAMILY: "Familia de tipografía del lector"
STR_EXT_READER_FONT: "Tipografía externa"
STR_EXT_CHINESE_FONT: "Tipografía (Lectura)"
STR_EXT_UI_FONT: "Tipografía (Pantalla)"
STR_FONT_SIZE: "Tamaño de la fuente (Pantalla)"
STR_LINE_SPACING: "Interlineado (Lectura)"
STR_ASCII_LETTER_SPACING: "Espaciado de letras ASCII"
STR_ASCII_DIGIT_SPACING: "Espaciado de dígitos ASCII"
STR_CJK_SPACING: "Espaciado CJK"
STR_COLOR_MODE: "Modo de color"
STR_SCREEN_MARGIN: "Margen de lectura"
STR_PARA_ALIGNMENT: "Ajuste de parágrafo del lector"
STR_HYPHENATION: "Hyphenation"
STR_TIME_TO_SLEEP: "Tiempo para dormir"
STR_REFRESH_FREQ: "Frecuencia de actualización"
STR_CALIBRE_SETTINGS: "Configuraciones de Calibre"
STR_KOREADER_SYNC: "Síncronización de KOReader"
STR_CHECK_UPDATES: "Verificar actualizaciones"
STR_LANGUAGE: "Idioma"
STR_SELECT_WALLPAPER: "Seleccionar fondo"
STR_CLEAR_READING_CACHE: "Borrar caché de lectura"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nombre de usuario"
STR_PASSWORD: "Contraseña"
STR_SYNC_SERVER_URL: "URL del servidor de síncronización"
STR_DOCUMENT_MATCHING: "Coincidencia de documentos"
STR_AUTHENTICATE: "Autentificar"
STR_KOREADER_USERNAME: "Nombre de usuario de KOReader"
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
STR_FILENAME: "Nombre del archivo"
STR_BINARY: "Binario"
STR_SET_CREDENTIALS_FIRST: "Configurar credenciales primero"
STR_WIFI_CONN_FAILED: "Falló la conexión Wi-Fi"
STR_AUTHENTICATING: "Autentificando..."
STR_AUTH_SUCCESS: "Autenticación exitsosa!"
STR_KOREADER_AUTH: "Autenticación KOReader"
STR_SYNC_READY: "La síncronización de KOReader está lista para usarse"
STR_AUTH_FAILED: "Falló la autenticación"
STR_DONE: "Hecho"
STR_CLEAR_CACHE_WARNING_1: "Esto borrará todos los datos en cache del libro."
STR_CLEAR_CACHE_WARNING_2: " ¡Se perderá todo el avance de leer!"
STR_CLEAR_CACHE_WARNING_3: "Los libros deberán ser reíndexados"
STR_CLEAR_CACHE_WARNING_4: "cuando se abran de nuevo."
STR_CLEARING_CACHE: "Borrando caché..."
STR_CACHE_CLEARED: "Cache limpia"
STR_ITEMS_REMOVED: "Elementos eliminados"
STR_FAILED_LOWER: "Falló"
STR_CLEAR_CACHE_FAILED: "No se pudo borrar la cache"
STR_CHECK_SERIAL_OUTPUT: "Verifique la salida serial para detalles"
STR_DARK: "Oscuro"
STR_LIGHT: "Claro"
STR_CUSTOM: "Personalizado"
STR_COVER: "Portada"
STR_NONE_OPT: "Ninguno"
STR_FIT: "Ajustar"
STR_CROP: "Recortar"
STR_NO_PROGRESS: "Sin avance"
STR_FULL_OPT: "Completa"
STR_NEVER: "Nunca"
STR_IN_READER: "En el lector"
STR_ALWAYS: "Siempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Dormir"
STR_PAGE_TURN: "Paso de página"
STR_PORTRAIT: "Portrato"
STR_LANDSCAPE_CW: "Paisaje sentido horario"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Paisaje sentido antihorario"
STR_FRONT_LAYOUT_BCLR: "Atrás, Confirmar, Izquierda, Derecha"
STR_FRONT_LAYOUT_LRBC: "Izquierda, Derecha, Atrás, Confirmar"
STR_FRONT_LAYOUT_LBCR: "Izquierda, Atrás, Confirmar, Derecha"
STR_PREV_NEXT: "Anterior/Siguiente"
STR_NEXT_PREV: "Siguiente/Anterior"
STR_BOOKERLY: "Relacionado con libros"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Pequeño"
STR_MEDIUM: "Medio"
STR_LARGE: "Grande"
STR_X_LARGE: "Extra grande"
STR_TIGHT: "Ajustado"
STR_NORMAL: "Normal"
STR_WIDE: "Ancho"
STR_JUSTIFY: "Justificar"
STR_ALIGN_LEFT: "Izquierda"
STR_CENTER: "Centro"
STR_ALIGN_RIGHT: "Derecha"
STR_MIN_1: "1 Minuto"
STR_MIN_5: "10 Minutos"
STR_MIN_10: "5 Minutos"
STR_MIN_15: "15 Minutos"
STR_MIN_30: "30 Minutos"
STR_PAGES_1: "1 Página"
STR_PAGES_5: "5 Páginas"
STR_PAGES_10: "10 Páginas"
STR_PAGES_15: "15 Páginas"
STR_PAGES_30: "30 Páginas"
STR_UPDATE: "ActualizaR"
STR_CHECKING_UPDATE: "Verificando actualización..."
STR_NEW_UPDATE: "¡Nueva actualización disponible!"
STR_CURRENT_VERSION: "Versión actual:"
STR_NEW_VERSION: "Nueva versión:"
STR_UPDATING: "Actualizando..."
STR_NO_UPDATE: "No hay actualizaciones disponibles"
STR_UPDATE_FAILED: "Falló la actualización"
STR_UPDATE_COMPLETE: "Actualización completada"
STR_POWER_ON_HINT: "Presione y mantenga presionado el botón de encendido para volver a encender"
STR_EXTERNAL_FONT: "Fuente externa"
STR_BUILTIN_DISABLED: "Incorporado (Desactivado)"
STR_NO_ENTRIES: "No se encontraron elementos"
STR_DOWNLOADING: "Descargando..."
STR_DOWNLOAD_FAILED: "Falló la descarga"
STR_ERROR_MSG: "Error"
STR_UNNAMED: "Sin nombre"
STR_NO_SERVER_URL: "No se ha configurado la url del servidor"
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_NETWORK_PREFIX: "Red: "
STR_IP_ADDRESS_PREFIX: "Dirección IP: "
STR_SCAN_QR_WIFI_HINT: "O escanee el código QR con su teléfono para conectarse a WI-FI."
STR_ERROR_GENERAL_FAILURE: "Error: Fallo general"
STR_ERROR_NETWORK_NOT_FOUND: "Error: Red no encontrada"
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
STR_SD_CARD: "Tarjeta SD"
STR_BACK: "« Atrás"
STR_EXIT: "« SaliR"
STR_HOME: "« Inicio"
STR_SAVE: "« Guardar"
STR_SELECT: "Seleccionar"
STR_TOGGLE: "Cambiar"
STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar"
STR_OPEN: "Abrir"
STR_DOWNLOAD: "Descargar"
STR_RETRY: "Reintentar"
STR_YES: "Sí"
STR_NO: "No"
STR_STATE_ON: "ENCENDIDO"
STR_STATE_OFF: "APAGADO"
STR_SET: "Configurar"
STR_NOT_SET: "No configurado"
STR_DIR_LEFT: "Izquierda"
STR_DIR_RIGHT: "Derecha"
STR_DIR_UP: "Arriba"
STR_DIR_DOWN: "Abajo"
STR_CAPS_ON: "MAYÚSCULAS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ENCENDIDO]"
STR_SLEEP_COVER_FILTER: "Filtro de salva pantalla y protección de la pantalla"
STR_FILTER_CONTRAST: "Contraste"
STR_STATUS_BAR_FULL_PERCENT: "Completa con porcentaje"
STR_STATUS_BAR_FULL_BOOK: "Completa con progreso del libro"
STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro"
STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos"
STR_UI_THEME: "Estilo de pantalla"
STR_THEME_CLASSIC: "Clásico"
STR_THEME_LYRA: "LYRA"
STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_OPDS_BROWSER: "Navegador opds"
STR_COVER_CUSTOM: "Portada + Personalizado"
STR_RECENTS: "Recientes"
STR_MENU_RECENT_BOOKS: "Libros recientes"
STR_NO_RECENT_BOOKS: "No hay libros recientes"
STR_CALIBRE_DESC: "Utilice las transferencias dispositivos inalámbricos de calibre"
STR_FORGET_AND_REMOVE: "Olvidar la red y eliminar la contraseña guardada?"
STR_FORGET_BUTTON: "Olvidar la red"
STR_CALIBRE_STARTING: "Iniciando calibre..."
STR_CALIBRE_SETUP: "Configuración"
STR_CALIBRE_STATUS: "Estado"
STR_CLEAR_BUTTON: "Borrar"
STR_DEFAULT_VALUE: "Previo"
STR_REMAP_PROMPT: "Presione un botón frontal para cada función"
STR_UNASSIGNED: "No asignado"
STR_ALREADY_ASSIGNED: "Ya asignado"
STR_REMAP_RESET_HINT: "Botón lateral arriba: Restablecer a la configuración previo"
STR_REMAP_CANCEL_HINT: "Botón lateral abajo: Anular reconfiguración"
STR_HW_BACK_LABEL: "Atrás (Primer botón)"
STR_HW_CONFIRM_LABEL: "Confirmar (Segundo botón)"
STR_HW_LEFT_LABEL: "Izquierda (Tercer botón)"
STR_HW_RIGHT_LABEL: "Derecha (Cuarto botón)"
STR_GO_TO_PERCENT: "Ir a %"
STR_GO_HOME_BUTTON: "Volver a inicio"
STR_SYNC_PROGRESS: "Progreso de síncronización"
STR_DELETE_CACHE: "Borrar cache del libro"
STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: " Páginas |"
STR_BOOK_PREFIX: "Libro:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "BLOQUEAR"
STR_CALIBRE_URL_HINT: "Para calibre, agregue /opds a su urL"
STR_PERCENT_STEP_HINT: "Izquierda/Derecha: 1% Arriba/Abajo: 10%"
STR_SYNCING_TIME: "Tiempo de síncronización..."
STR_CALC_HASH: "Calculando hash del documento..."
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
STR_FETCH_PROGRESS: "Recuperando progreso remoto..."
STR_UPLOAD_PROGRESS: "Subiendo progreso..."
STR_NO_CREDENTIALS_MSG: "No se han configurado credenciales"
STR_KOREADER_SETUP_HINT: "Configure una cuenta de KOReader en la configuración"
STR_PROGRESS_FOUND: "¡Progreso encontrado!"
STR_REMOTE_LABEL: "Remoto"
STR_LOCAL_LABEL: "Local"
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% Completada"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d / %d, %.2f% Completada"
STR_DEVICE_FROM_FORMAT: " De: %s"
STR_APPLY_REMOTE: "Aplicar progreso remoto"
STR_UPLOAD_LOCAL: "Subir progreso local"
STR_NO_REMOTE_MSG: "No se encontró progreso remoto"
STR_UPLOAD_PROMPT: "Subir posicion actual?"
STR_UPLOAD_SUCCESS: "¡Progreso subido!"
STR_SYNC_FAILED_MSG: "Fallo de síncronización"
STR_SECTION_PREFIX: "Seccion"
STR_UPLOAD: "Subir"
STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_CHOOSE_SOMETHING: "Elige algo para leer"
STR_CLOCK: "Reloj"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 horas"
STR_SET_TIME: "Establecer hora"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -0,0 +1,337 @@
_language_name: "Svenska"
_language_code: "SWEDISH"
_order: "7"
STR_CROSSPOINT: "Crosspoint"
STR_BOOTING: "STARTAR"
STR_SLEEPING: "VILA"
STR_ENTERING_SLEEP: "Går i vila…"
STR_BROWSE_FILES: "Bläddra filer…"
STR_FILE_TRANSFER: "Filöverföring"
STR_SETTINGS_TITLE: "Inställningar"
STR_CALIBRE_LIBRARY: "Calibrebibliotek"
STR_CONTINUE_READING: "Fortsätt läsa"
STR_NO_OPEN_BOOK: "Ingen öppen bok"
STR_START_READING: "Börja läsa nedan"
STR_BOOKS: "Böcker"
STR_NO_BOOKS_FOUND: "Inga böcker hittade"
STR_SELECT_CHAPTER: "Välj kapitel"
STR_NO_CHAPTERS: "Inga kapitel"
STR_END_OF_BOOK: "Slutet på boken"
STR_EMPTY_CHAPTER: "Tomt kapitel"
STR_INDEXING: "Indexerar…"
STR_MEMORY_ERROR: "Minnesfel"
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
STR_EMPTY_FILE: "Tom fil"
STR_OUT_OF_BOUNDS: "Utanför gränserna"
STR_LOADING: "Laddar…"
STR_LOAD_XTC_FAILED: "Misslyckades ladda XTC"
STR_LOAD_TXT_FAILED: "Misslyckades ladda TCT"
STR_LOAD_EPUB_FAILED: "Misslyckades ladda EPUB"
STR_SD_CARD_ERROR: "SD-kortfel"
STR_WIFI_NETWORKS: "Trådlösa nätverk"
STR_NO_NETWORKS: "Inga nätverk funna"
STR_NETWORKS_FOUND: "%zu nätverk funna"
STR_SCANNING: "Scannar…"
STR_CONNECTING: "Ansluter…"
STR_CONNECTED: "Ansluten!"
STR_CONNECTION_FAILED: "Anslutning misslyckades"
STR_CONNECTION_TIMEOUT: "Anslutnings timeout"
STR_FORGET_NETWORK: "Glöm nätverk?"
STR_SAVE_PASSWORD: "Spara lösenord till nästa gång?"
STR_REMOVE_PASSWORD: "Radera sparat lösenord?"
STR_PRESS_OK_SCAN: "Tryck OK för att skanna igen"
STR_PRESS_ANY_CONTINUE: "Tryck valfri knapp för att fortsätta"
STR_SELECT_HINT: "VÄNSTER/HÖGER: Välj OK: Bekräfta"
STR_HOW_CONNECT: "Hur vill du ansluta?"
STR_JOIN_NETWORK: "Anslut till ett nätverk"
STR_CREATE_HOTSPOT: "Skapa surfzon"
STR_JOIN_DESC: "Anslut till ett befintligt trådlöst nätverk"
STR_HOTSPOT_DESC: "Skapa ett trådlöst nätverk andra kan ansluta till"
STR_STARTING_HOTSPOT: "Startar surfzon…"
STR_HOTSPOT_MODE: "Surfzonsläge"
STR_CONNECT_WIFI_HINT: "Anslut din enhet till detta trådlösa nätverk"
STR_OPEN_URL_HINT: "Öppna denna adress i din browser"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
STR_CALIBRE_WIRELESS: "Calibre Trådlöst"
STR_CALIBRE_WEB_URL: "Calibre webbadress"
STR_CONNECT_WIRELESS: "Anslut som trådlös enhet"
STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
STR_MAC_ADDRESS: "MAC-adress:"
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
STR_ENTER_WIFI_PASSWORD: "Skriv in WiFi-lösenord"
STR_ENTER_TEXT: "Skriv text"
STR_TO_PREFIX: "till"
STR_CALIBRE_DISCOVERING: "Söker Calibre…"
STR_CALIBRE_CONNECTING_TO: "Ansluter till"
STR_CALIBRE_CONNECTED_TO: "Ansluten till"
STR_CALIBRE_WAITING_COMMANDS: "Väntar på kommandon…"
STR_CONNECTION_FAILED_RETRYING: "(Anslutning misslyckades. Försöker igen)"
STR_CALIBRE_DISCONNECTED: "Calibre nedkopplat"
STR_CALIBRE_WAITING_TRANSFER: "Väntar på överföring…"
STR_CALIBRE_TRANSFER_HINT: "Om överföring misslyckas: Aktivera\\n'Ignorera fritt utrymme' i Calibre's\\nSmartDevice plugin settings."
STR_CALIBRE_RECEIVING: "Tar emot:"
STR_CALIBRE_RECEIVED: "Mottaget:"
STR_CALIBRE_WAITING_MORE: "Väntar på mer.."
STR_CALIBRE_FAILED_CREATE_FILE: "Misslyckades att skapa fil"
STR_CALIBRE_PASSWORD_REQUIRED: "Lösenord krävs"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Överföring avbröts"
STR_CALIBRE_INSTRUCTION_1: "1) Installera CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Anslut till samma trådlösa nätverk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: ”Skicka till enhet”"
STR_CALIBRE_INSTRUCTION_4: "”Håll denna skärm öppen under sändning”"
STR_CAT_DISPLAY: "Skärm"
STR_CAT_READER: "Läsare"
STR_CAT_CONTROLS: "Kontroller"
STR_CAT_SYSTEM: "System"
STR_CAT_CLOCK: "Klocka"
STR_SLEEP_SCREEN: "Viloskärm"
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
STR_STATUS_BAR: "Statusrad"
STR_HIDE_BATTERY: "Dölj batteriprocent"
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
STR_TEXT_AA: "Textkantutjämning"
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
STR_ORIENTATION: "Läsrikting"
STR_FRONT_BTN_LAYOUT: "Frontknappslayout"
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
STR_LONG_PRESS_SKIP: "Lång-tryck Kapitelskippning"
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
STR_EXT_READER_FONT: "Extern Eboksläsartypsnitt"
STR_EXT_CHINESE_FONT: "Eboksläsartypsnitt"
STR_EXT_UI_FONT: "Användargränssnittets typsnitt"
STR_FONT_SIZE: "Användargränssnittets typsnittsstorlek"
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
STR_ASCII_LETTER_SPACING: "ASCII-bokstavsmellanrum"
STR_ASCII_DIGIT_SPACING: "ASCII-siffermellanrum"
STR_CJK_SPACING: "CJK-mellanrum"
STR_COLOR_MODE: "Färgläge"
STR_SCREEN_MARGIN: "Eboksläsarens skärmmarginal"
STR_PARA_ALIGNMENT: "Eboksläsarens paragraflinjeplacering"
STR_HYPHENATION: "Avstavning"
STR_TIME_TO_SLEEP: "Tid för att gå i vila"
STR_REFRESH_FREQ: "Uppdateringsfrekvens"
STR_CALIBRE_SETTINGS: "Calibreinställningar"
STR_KOREADER_SYNC: "KorReader-synkronisering"
STR_CHECK_UPDATES: "Kolla efter uppdateringar"
STR_LANGUAGE: "Språk"
STR_SELECT_WALLPAPER: "Välj bakgrundsbild"
STR_CLEAR_READING_CACHE: "Rensa Eboksläsarens cache"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Användarnamn"
STR_PASSWORD: "Lösenord"
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
STR_AUTHENTICATE: "Autentisera "
STR_KOREADER_USERNAME: "KOReader användarnamn"
STR_KOREADER_PASSWORD: "KOReader lösenord"
STR_FILENAME: "Filnamn"
STR_BINARY: "Binär"
STR_SET_CREDENTIALS_FIRST: "Referenser"
STR_WIFI_CONN_FAILED: "Trådlös anslutning misslyckades"
STR_AUTHENTICATING: "Autentiserar…"
STR_AUTH_SUCCESS: "Lyckad autentisering!"
STR_KOREADER_AUTH: "KORreader autentisering"
STR_SYNC_READY: "KOReader synk är redo att användas"
STR_AUTH_FAILED: "Autentisering misslyckades"
STR_DONE: "Klar"
STR_CLEAR_CACHE_WARNING_1: "Detta rensar all cachad bokdata"
STR_CLEAR_CACHE_WARNING_2: "Alla läsframsteg kommer att försvinna!"
STR_CLEAR_CACHE_WARNING_3: "Böcker kommer att behöva omindexeras"
STR_CLEAR_CACHE_WARNING_4: "när de öppnas på nytt."
STR_CLEARING_CACHE: "Rensar cache…"
STR_CACHE_CLEARED: "Cache rensad!"
STR_ITEMS_REMOVED: "objekt raderade"
STR_FAILED_LOWER: "misslyckades "
STR_CLEAR_CACHE_FAILED: "Misslyckades att rensa cache"
STR_CHECK_SERIAL_OUTPUT: "Kolla seriell utgång för detaljer"
STR_DARK: "Mörk"
STR_LIGHT: "Ljus"
STR_CUSTOM: "Valfri"
STR_COVER: "Omslag"
STR_NONE_OPT: "Ingen öppen bok"
STR_FIT: "Passa"
STR_CROP: "Beskär"
STR_NO_PROGRESS: "Ingen framgång"
STR_FULL_OPT: "Full"
STR_NEVER: "Aldrig"
STR_IN_READER: "I Eboksläsare"
STR_ALWAYS: "Alltid"
STR_IGNORE: "Ignorera"
STR_SLEEP: "Vila"
STR_PAGE_TURN: "Sidvändning"
STR_PORTRAIT: "Porträtt"
STR_LANDSCAPE_CW: "Landskap medurs"
STR_INVERTED: "Inverterad"
STR_LANDSCAPE_CCW: "Landskap moturs"
STR_FRONT_LAYOUT_BCLR: "Bak, Bekr,Vän, Hög"
STR_FRONT_LAYOUT_LRBC: "Vän, Hög, Bak, Bekr"
STR_FRONT_LAYOUT_LBCR: "Vän, Bak, Bekr, Hög"
STR_PREV_NEXT: "Förra/Nästa"
STR_NEXT_PREV: "Nästa/Förra"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Öppen dyslektisk"
STR_SMALL: "Liten"
STR_MEDIUM: "Medium"
STR_LARGE: "Stor"
STR_X_LARGE: "Extra stor"
STR_TIGHT: "Smal"
STR_NORMAL: "Normal"
STR_WIDE: "Bred"
STR_JUSTIFY: "Rättfärdiga"
STR_ALIGN_LEFT: "Vänster"
STR_CENTER: "Mitten"
STR_ALIGN_RIGHT: "Höger"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 sida"
STR_PAGES_5: "5 sidor"
STR_PAGES_10: "10 sidor"
STR_PAGES_15: "15 sidor"
STR_PAGES_30: "30 sidor"
STR_UPDATE: "Uppdatera"
STR_CHECKING_UPDATE: "Söker uppdatering…"
STR_NEW_UPDATE: "Ny uppdatering tillgänglig!"
STR_CURRENT_VERSION: "Nuvarande version:"
STR_NEW_VERSION: "Ny version:"
STR_UPDATING: "Uppdaterar…"
STR_NO_UPDATE: "Ingen uppdatering tillgänglig"
STR_UPDATE_FAILED: "Uppdatering misslyckades"
STR_UPDATE_COMPLETE: "Uppdatering färdig"
STR_POWER_ON_HINT: "Tryck och håll strömknappen för att sätta på igen"
STR_EXTERNAL_FONT: "Externt typsnitt"
STR_BUILTIN_DISABLED: "Inbyggd (Avstängd)"
STR_NO_ENTRIES: "Inga poster funna"
STR_DOWNLOADING: "Laddar ner…"
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
STR_ERROR_MSG: "Fel:"
STR_UNNAMED: "Ej namngiven"
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
STR_NETWORK_PREFIX: "Nätverk:"
STR_IP_ADDRESS_PREFIX: "IP-adress;"
STR_SCAN_QR_WIFI_HINT: "eller skanna QR-kod med din telefon för att ansluta till WiFi."
STR_ERROR_GENERAL_FAILURE: "Fel: Generellt fel"
STR_ERROR_NETWORK_NOT_FOUND: "Fel: Nätverk hittades inte"
STR_ERROR_CONNECTION_TIMEOUT: "Fel: Anslutningstimeout"
STR_SD_CARD: "SD-kort"
STR_BACK: "« Bak"
STR_EXIT: "« Avsluta"
STR_HOME: "« Hem"
STR_SAVE: "« Spara"
STR_SELECT: "Välj "
STR_TOGGLE: "Växla"
STR_CONFIRM: "Bekräfta"
STR_CANCEL: "Avbryt"
STR_CONNECT: "Anslut"
STR_OPEN: "Öppna"
STR_DOWNLOAD: "Ladda ner"
STR_RETRY: "Försök igen"
STR_YES: "Ja"
STR_NO: "Nej"
STR_STATE_ON: "PÅ"
STR_STATE_OFF: "AV"
STR_SET: "Inställd"
STR_NOT_SET: "Inte inställd"
STR_DIR_LEFT: "Vänster"
STR_DIR_RIGHT: "Höger"
STR_DIR_UP: "Upp"
STR_DIR_DOWN: "Ner"
STR_CAPS_ON: "VERSALER"
STR_CAPS_OFF: "versaler"
STR_OK_BUTTON: "Okej"
STR_ON_MARKER: "[PÅ]"
STR_SLEEP_COVER_FILTER: "Viloskärmens omslagsfilter"
STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Procent"
STR_STATUS_BAR_FULL_BOOK: "Full w/ Boklist"
STR_STATUS_BAR_BOOK_ONLY: "Boklist enbart"
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist"
STR_UI_THEME: "Användargränssnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_OPDS_BROWSER: "OPDS-webbläsare"
STR_COVER_CUSTOM: "Omslag + Valfri"
STR_RECENTS: "Senaste"
STR_MENU_RECENT_BOOKS: "Senaste böckerna"
STR_NO_RECENT_BOOKS: "Inga senaste böcker"
STR_CALIBRE_DESC: "Använd Calibres trådlösa enhetsöverföring"
STR_FORGET_AND_REMOVE: "Glöm nätverk och ta bort sparat lösenord?"
STR_FORGET_BUTTON: "Glöm nätverk"
STR_CALIBRE_STARTING: "Starar Calibre…"
STR_CALIBRE_SETUP: "Inställning"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Rensa"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Tryck en frontknapp för var funktion"
STR_UNASSIGNED: "Otilldelad"
STR_ALREADY_ASSIGNED: "Redan tilldelad"
STR_REMAP_RESET_HINT: "Översta sidoknapp: Återställ standardlayout"
STR_REMAP_CANCEL_HINT: "Nedre sidoknapp: Avbryt tilldelning"
STR_HW_BACK_LABEL: "Bak (Första knapp)"
STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knapp)"
STR_HW_LEFT_LABEL: "Vänster (Tredje knapp)"
STR_HW_RIGHT_LABEL: "Höger (Fjärde knapp)"
STR_GO_TO_PERCENT: "Gå till %"
STR_GO_HOME_BUTTON: "Gå Hem"
STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
STR_DELETE_CACHE: "Radera bokcache"
STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " sidor | "
STR_BOOK_PREFIX: "Bok:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
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_CALC_HASH: "Beräknar dokumenthash"
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
STR_FETCH_PROGRESS: "Hämtar fjärrframsteg"
STR_UPLOAD_PROGRESS: "Laddar upp framsteg"
STR_NO_CREDENTIALS_MSG: "Inga uppgifter inställda"
STR_KOREADER_SETUP_HINT: "Ställ in KOReaderkonto i Inställningar"
STR_PROGRESS_FOUND: "Framsteg funna!"
STR_REMOTE_LABEL: "Fjärr:"
STR_LOCAL_LABEL: "Lokalt:"
STR_PAGE_OVERALL_FORMAT: "Sida %d, %.2f%% totalt"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Sida %d/%d, %.2f%% totalt"
STR_DEVICE_FROM_FORMAT: " Från: %s"
STR_APPLY_REMOTE: "Använd fjärrframsteg"
STR_UPLOAD_LOCAL: "Ladda upp lokala framsteg"
STR_NO_REMOTE_MSG: "Inga fjärrframsteg funna"
STR_UPLOAD_PROMPT: "Ladda upp nuvarande position?"
STR_UPLOAD_SUCCESS: "Framsteg uppladdade!"
STR_SYNC_FAILED_MSG: "Synkronisering misslyckades"
STR_SECTION_PREFIX: "Sektion"
STR_UPLOAD: "Uppladdning"
STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil"
STR_OPDS_SERVER_URL: "OPDS-serveradress"
STR_CHOOSE_SOMETHING: "Välj något att läsa"
STR_CLOCK: "Klocka"
STR_CLOCK_AMPM: "AM/PM"
STR_CLOCK_24H: "24 timmar"
STR_SET_TIME: "Ställ in tid"
STR_CLOCK_SIZE: "Clock Size"
STR_CLOCK_SIZE_SMALL: "Small"
STR_CLOCK_SIZE_MEDIUM: "Medium"
STR_CLOCK_SIZE_LARGE: "Large"
STR_TIMEZONE: "Timezone"
STR_TZ_UTC: "UTC"
STR_TZ_EASTERN: "Eastern"
STR_TZ_CENTRAL: "Central"
STR_TZ_MOUNTAIN: "Mountain"
STR_TZ_PACIFIC: "Pacific"
STR_TZ_ALASKA: "Alaska"
STR_TZ_HAWAII: "Hawaii"
STR_TZ_CUSTOM: "Custom"
STR_SET_UTC_OFFSET: "Set UTC Offset"

View File

@@ -1,7 +1,7 @@
#include "JpegToBmpConverter.h" #include "JpegToBmpConverter.h"
#include <HardwareSerial.h> #include <HalStorage.h>
#include <SdFat.h> #include <Logging.h>
#include <picojpeg.h> #include <picojpeg.h>
#include <cstdio> #include <cstdio>
@@ -201,8 +201,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
// Internal implementation with configurable target size and bit depth // Internal implementation with configurable target size and bit depth
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
bool oneBit, bool crop) { bool oneBit, bool crop) {
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit", LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
targetWidth, targetHeight);
// Setup context for picojpeg callback // Setup context for picojpeg callback
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0}; JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
@@ -211,12 +210,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
pjpeg_image_info_t imageInfo; pjpeg_image_info_t imageInfo;
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0); const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
if (status != 0) { if (status != 0) {
Serial.printf("[%lu] [JPG] JPEG decode init failed with error code: %d\n", millis(), status); LOG_ERR("JPG", "JPEG decode init failed with error code: %d", status);
return false; return false;
} }
Serial.printf("[%lu] [JPG] JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d\n", millis(), imageInfo.m_width, LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height,
imageInfo.m_height, imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol); imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
// Safety limits to prevent memory issues on ESP32 // Safety limits to prevent memory issues on ESP32
constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_WIDTH = 2048;
@@ -224,8 +223,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
constexpr int MAX_MCU_ROW_BYTES = 65536; constexpr int MAX_MCU_ROW_BYTES = 65536;
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) { if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
Serial.printf("[%lu] [JPG] Image too large (%dx%d), max supported: %dx%d\n", millis(), imageInfo.m_width, LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height,
imageInfo.m_height, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
return false; return false;
} }
@@ -262,8 +261,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight; scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
needsScaling = true; needsScaling = true;
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width, LOG_DBG("JPG", "Pre-scaling %dx%d -> %dx%d (fit to %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth,
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight); outHeight, targetWidth, targetHeight);
} }
// Write BMP header with output dimensions // Write BMP header with output dimensions
@@ -282,7 +281,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
// Allocate row buffer // Allocate row buffer
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow)); auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
if (!rowBuffer) { if (!rowBuffer) {
Serial.printf("[%lu] [JPG] Failed to allocate row buffer\n", millis()); LOG_ERR("JPG", "Failed to allocate row buffer");
return false; return false;
} }
@@ -293,15 +292,14 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
// Validate MCU row buffer size before allocation // Validate MCU row buffer size before allocation
if (mcuRowPixels > MAX_MCU_ROW_BYTES) { if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
Serial.printf("[%lu] [JPG] MCU row buffer too large (%d bytes), max: %d\n", millis(), mcuRowPixels, LOG_DBG("JPG", "MCU row buffer too large (%d bytes), max: %d", mcuRowPixels, MAX_MCU_ROW_BYTES);
MAX_MCU_ROW_BYTES);
free(rowBuffer); free(rowBuffer);
return false; return false;
} }
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels)); auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
if (!mcuRowBuffer) { if (!mcuRowBuffer) {
Serial.printf("[%lu] [JPG] Failed to allocate MCU row buffer (%d bytes)\n", millis(), mcuRowPixels); LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels);
free(rowBuffer); free(rowBuffer);
return false; return false;
} }
@@ -349,10 +347,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
const unsigned char mcuStatus = pjpeg_decode_mcu(); const unsigned char mcuStatus = pjpeg_decode_mcu();
if (mcuStatus != 0) { if (mcuStatus != 0) {
if (mcuStatus == PJPG_NO_MORE_BLOCKS) { if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
Serial.printf("[%lu] [JPG] Unexpected end of blocks at MCU (%d, %d)\n", millis(), mcuX, mcuY); LOG_ERR("JPG", "Unexpected end of blocks at MCU (%d, %d)", mcuX, mcuY);
} else { } else {
Serial.printf("[%lu] [JPG] JPEG decode MCU failed at (%d, %d) with error code: %d\n", millis(), mcuX, mcuY, LOG_ERR("JPG", "JPEG decode MCU failed at (%d, %d) with error code: %d", mcuX, mcuY, mcuStatus);
mcuStatus);
} }
free(mcuRowBuffer); free(mcuRowBuffer);
free(rowBuffer); free(rowBuffer);
@@ -549,7 +546,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
free(mcuRowBuffer); free(mcuRowBuffer);
free(rowBuffer); free(rowBuffer);
Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis()); LOG_DBG("JPG", "Successfully converted JPEG to BMP");
return true; return true;
} }
@@ -567,5 +564,5 @@ bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bm
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth,
int targetMaxHeight) { int targetMaxHeight) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true); return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
} }

View File

@@ -1,8 +1,8 @@
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
#include <HardwareSerial.h> #include <HalStorage.h>
#include <Logging.h>
#include <MD5Builder.h> #include <MD5Builder.h>
#include <SDCardManager.h>
#include <Serialization.h> #include <Serialization.h>
// Initialize the static instance // Initialize the static instance
@@ -32,10 +32,10 @@ void KOReaderCredentialStore::obfuscate(std::string& data) const {
bool KOReaderCredentialStore::saveToFile() const { bool KOReaderCredentialStore::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
SdMan.mkdir("/.crosspoint"); Storage.mkdir("/.crosspoint");
FsFile file; FsFile file;
if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) { if (!Storage.openFileForWrite("KRS", KOREADER_FILE, file)) {
return false; return false;
} }
@@ -44,7 +44,7 @@ bool KOReaderCredentialStore::saveToFile() const {
// Write username (plaintext - not particularly sensitive) // Write username (plaintext - not particularly sensitive)
serialization::writeString(file, username); serialization::writeString(file, username);
Serial.printf("[%lu] [KRS] Saving username: %s\n", millis(), username.c_str()); LOG_DBG("KRS", "Saving username: %s", username.c_str());
// Write password (obfuscated) // Write password (obfuscated)
std::string obfuscatedPwd = password; std::string obfuscatedPwd = password;
@@ -58,14 +58,14 @@ bool KOReaderCredentialStore::saveToFile() const {
serialization::writePod(file, static_cast<uint8_t>(matchMethod)); serialization::writePod(file, static_cast<uint8_t>(matchMethod));
file.close(); file.close();
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis()); LOG_DBG("KRS", "Saved KOReader credentials to file");
return true; return true;
} }
bool KOReaderCredentialStore::loadFromFile() { bool KOReaderCredentialStore::loadFromFile() {
FsFile file; FsFile file;
if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) { if (!Storage.openFileForRead("KRS", KOREADER_FILE, file)) {
Serial.printf("[%lu] [KRS] No credentials file found\n", millis()); LOG_DBG("KRS", "No credentials file found");
return false; return false;
} }
@@ -73,7 +73,7 @@ bool KOReaderCredentialStore::loadFromFile() {
uint8_t version; uint8_t version;
serialization::readPod(file, version); serialization::readPod(file, version);
if (version != KOREADER_FILE_VERSION) { if (version != KOREADER_FILE_VERSION) {
Serial.printf("[%lu] [KRS] Unknown file version: %u\n", millis(), version); LOG_DBG("KRS", "Unknown file version: %u", version);
file.close(); file.close();
return false; return false;
} }
@@ -110,14 +110,14 @@ bool KOReaderCredentialStore::loadFromFile() {
} }
file.close(); file.close();
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str()); LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", username.c_str());
return true; return true;
} }
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) { void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
username = user; username = user;
password = pass; password = pass;
Serial.printf("[%lu] [KRS] Set credentials for user: %s\n", millis(), user.c_str()); LOG_DBG("KRS", "Set credentials for user: %s", user.c_str());
} }
std::string KOReaderCredentialStore::getMd5Password() const { std::string KOReaderCredentialStore::getMd5Password() const {
@@ -140,12 +140,12 @@ void KOReaderCredentialStore::clearCredentials() {
username.clear(); username.clear();
password.clear(); password.clear();
saveToFile(); saveToFile();
Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis()); LOG_DBG("KRS", "Cleared KOReader credentials");
} }
void KOReaderCredentialStore::setServerUrl(const std::string& url) { void KOReaderCredentialStore::setServerUrl(const std::string& url) {
serverUrl = url; serverUrl = url;
Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str()); LOG_DBG("KRS", "Set server URL: %s", url.empty() ? "(default)" : url.c_str());
} }
std::string KOReaderCredentialStore::getBaseUrl() const { std::string KOReaderCredentialStore::getBaseUrl() const {
@@ -163,6 +163,5 @@ std::string KOReaderCredentialStore::getBaseUrl() const {
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) { void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
matchMethod = method; matchMethod = method;
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(), LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
} }

View File

@@ -1,8 +1,8 @@
#include "KOReaderDocumentId.h" #include "KOReaderDocumentId.h"
#include <HardwareSerial.h> #include <HalStorage.h>
#include <Logging.h>
#include <MD5Builder.h> #include <MD5Builder.h>
#include <SDCardManager.h>
namespace { namespace {
// Extract filename from path (everything after last '/') // Extract filename from path (everything after last '/')
@@ -27,7 +27,7 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat
md5.calculate(); md5.calculate();
std::string result = md5.toString().c_str(); std::string result = md5.toString().c_str();
Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str()); LOG_DBG("KODoc", "Filename hash: %s (from '%s')", result.c_str(), filename.c_str());
return result; return result;
} }
@@ -43,13 +43,13 @@ size_t KOReaderDocumentId::getOffset(int i) {
std::string KOReaderDocumentId::calculate(const std::string& filePath) { std::string KOReaderDocumentId::calculate(const std::string& filePath) {
FsFile file; FsFile file;
if (!SdMan.openFileForRead("KODoc", filePath, file)) { if (!Storage.openFileForRead("KODoc", filePath, file)) {
Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str()); LOG_DBG("KODoc", "Failed to open file: %s", filePath.c_str());
return ""; return "";
} }
const size_t fileSize = file.fileSize(); const size_t fileSize = file.fileSize();
Serial.printf("[%lu] [KODoc] Calculating hash for file: %s (size: %zu)\n", millis(), filePath.c_str(), fileSize); LOG_DBG("KODoc", "Calculating hash for file: %s (size: %zu)", filePath.c_str(), fileSize);
// Initialize MD5 builder // Initialize MD5 builder
MD5Builder md5; MD5Builder md5;
@@ -70,7 +70,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
// Seek to offset // Seek to offset
if (!file.seekSet(offset)) { if (!file.seekSet(offset)) {
Serial.printf("[%lu] [KODoc] Failed to seek to offset %zu\n", millis(), offset); LOG_DBG("KODoc", "Failed to seek to offset %zu", offset);
continue; continue;
} }
@@ -90,7 +90,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
md5.calculate(); md5.calculate();
std::string result = md5.toString().c_str(); std::string result = md5.toString().c_str();
Serial.printf("[%lu] [KODoc] Hash calculated: %s (from %zu bytes)\n", millis(), result.c_str(), totalBytesRead); LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead);
return result; return result;
} }

View File

@@ -2,7 +2,7 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <HardwareSerial.h> #include <Logging.h>
#include <WiFi.h> #include <WiFi.h>
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
@@ -19,6 +19,10 @@ void addAuthHeaders(HTTPClient& http) {
http.addHeader("Accept", "application/vnd.koreader.v1+json"); http.addHeader("Accept", "application/vnd.koreader.v1+json");
http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str()); http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str());
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str()); http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str());
// HTTP Basic Auth (RFC 7617) header. This is needed to support koreader sync server embedded in Calibre Web Automated
// (https://github.com/crocodilestick/Calibre-Web-Automated/blob/main/cps/progress_syncing/protocols/kosync.py)
http.setAuthorization(KOREADER_STORE.getUsername().c_str(), KOREADER_STORE.getPassword().c_str());
} }
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; } bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
@@ -26,12 +30,12 @@ bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0;
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis()); LOG_DBG("KOSync", "No credentials configured");
return NO_CREDENTIALS; return NO_CREDENTIALS;
} }
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str()); LOG_DBG("KOSync", "Authenticating: %s", url.c_str());
HTTPClient http; HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient; std::unique_ptr<WiFiClientSecure> secureClient;
@@ -49,7 +53,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
const int httpCode = http.GET(); const int httpCode = http.GET();
http.end(); http.end();
Serial.printf("[%lu] [KOSync] Auth response: %d\n", millis(), httpCode); LOG_DBG("KOSync", "Auth response: %d", httpCode);
if (httpCode == 200) { if (httpCode == 200) {
return OK; return OK;
@@ -64,12 +68,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash, KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
KOReaderProgress& outProgress) { KOReaderProgress& outProgress) {
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis()); LOG_DBG("KOSync", "No credentials configured");
return NO_CREDENTIALS; return NO_CREDENTIALS;
} }
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str()); LOG_DBG("KOSync", "Getting progress: %s", url.c_str());
HTTPClient http; HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient; std::unique_ptr<WiFiClientSecure> secureClient;
@@ -95,7 +99,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
const DeserializationError error = deserializeJson(doc, responseBody); const DeserializationError error = deserializeJson(doc, responseBody);
if (error) { if (error) {
Serial.printf("[%lu] [KOSync] JSON parse failed: %s\n", millis(), error.c_str()); LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
return JSON_ERROR; return JSON_ERROR;
} }
@@ -106,14 +110,13 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
outProgress.deviceId = doc["device_id"].as<std::string>(); outProgress.deviceId = doc["device_id"].as<std::string>();
outProgress.timestamp = doc["timestamp"].as<int64_t>(); outProgress.timestamp = doc["timestamp"].as<int64_t>();
Serial.printf("[%lu] [KOSync] Got progress: %.2f%% at %s\n", millis(), outProgress.percentage * 100, LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
outProgress.progress.c_str());
return OK; return OK;
} }
http.end(); http.end();
Serial.printf("[%lu] [KOSync] Get progress response: %d\n", millis(), httpCode); LOG_DBG("KOSync", "Get progress response: %d", httpCode);
if (httpCode == 401) { if (httpCode == 401) {
return AUTH_FAILED; return AUTH_FAILED;
@@ -127,12 +130,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) { KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis()); LOG_DBG("KOSync", "No credentials configured");
return NO_CREDENTIALS; return NO_CREDENTIALS;
} }
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str()); LOG_DBG("KOSync", "Updating progress: %s", url.c_str());
HTTPClient http; HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient; std::unique_ptr<WiFiClientSecure> secureClient;
@@ -159,12 +162,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
std::string body; std::string body;
serializeJson(doc, body); serializeJson(doc, body);
Serial.printf("[%lu] [KOSync] Request body: %s\n", millis(), body.c_str()); LOG_DBG("KOSync", "Request body: %s", body.c_str());
const int httpCode = http.PUT(body.c_str()); const int httpCode = http.PUT(body.c_str());
http.end(); http.end();
Serial.printf("[%lu] [KOSync] Update progress response: %d\n", millis(), httpCode); LOG_DBG("KOSync", "Update progress response: %d", httpCode);
if (httpCode == 200 || httpCode == 202) { if (httpCode == 200 || httpCode == 202) {
return OK; return OK;

View File

@@ -1,6 +1,6 @@
#include "ProgressMapper.h" #include "ProgressMapper.h"
#include <HardwareSerial.h> #include <Logging.h>
#include <cmath> #include <cmath>
@@ -23,8 +23,8 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex); const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown"; const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
Serial.printf("[%lu] [ProgressMapper] CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s\n", millis(), LOG_DBG("ProgressMapper", "CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s", chapterName.c_str(),
chapterName.c_str(), pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str()); pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
return result; return result;
} }
@@ -76,8 +76,8 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
} }
} }
Serial.printf("[%lu] [ProgressMapper] KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d\n", millis(), LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d", koPos.percentage * 100,
koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber); koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
return result; return result;
} }

47
lib/Logging/Logging.cpp Normal file
View File

@@ -0,0 +1,47 @@
#include "Logging.h"
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
// provide the format string for the message itself.
void logPrintf(const char* level, const char* origin, const char* format, ...) {
if (!logSerial) {
return; // Serial not initialized, skip logging
}
va_list args;
va_start(args, format);
char buf[256];
char* c = buf;
// add the timestamp
{
unsigned long ms = millis();
int len = snprintf(c, sizeof(buf), "[%lu] ", ms);
if (len < 0) {
return; // encoding error, skip logging
}
c += len;
}
// add the level
{
const char* p = level;
size_t remaining = sizeof(buf) - (c - buf);
while (*p && remaining > 1) {
*c++ = *p++;
remaining--;
}
if (remaining > 1) {
*c++ = ' ';
}
}
// add the origin
{
int len = snprintf(c, sizeof(buf) - (c - buf), "[%s] ", origin);
if (len < 0) {
return; // encoding error, skip logging
}
c += len;
}
// add the user message
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
va_end(args);
logSerial.print(buf);
}

71
lib/Logging/Logging.h Normal file
View File

@@ -0,0 +1,71 @@
#pragma once
#include <HardwareSerial.h>
/*
Define ENABLE_SERIAL_LOG to enable logging
Can be set in platformio.ini build_flags or as a compile definition
Define LOG_LEVEL to control log verbosity:
0 = ERR only
1 = ERR + INF
2 = ERR + INF + DBG
If not defined, defaults to 0
If you have a legitimate need for raw Serial access (e.g., binary data,
special formatting), use the underlying logSerial object directly:
logSerial.printf("Special case: %d\n", value);
logSerial.write(binaryData, length);
The logSerial reference (defined below) points to the real Serial object and
won't trigger deprecation warnings.
*/
#ifndef LOG_LEVEL
#define LOG_LEVEL 0
#endif
static HWCDC& logSerial = Serial;
void logPrintf(const char* level, const char* origin, const char* format, ...);
#ifdef ENABLE_SERIAL_LOG
#if LOG_LEVEL >= 0
#define LOG_ERR(origin, format, ...) logPrintf("[ERR]", origin, format "\n", ##__VA_ARGS__)
#else
#define LOG_ERR(origin, format, ...)
#endif
#if LOG_LEVEL >= 1
#define LOG_INF(origin, format, ...) logPrintf("[INF]", origin, format "\n", ##__VA_ARGS__)
#else
#define LOG_INF(origin, format, ...)
#endif
#if LOG_LEVEL >= 2
#define LOG_DBG(origin, format, ...) logPrintf("[DBG]", origin, format "\n", ##__VA_ARGS__)
#else
#define LOG_DBG(origin, format, ...)
#endif
#else
#define LOG_DBG(origin, format, ...)
#define LOG_ERR(origin, format, ...)
#define LOG_INF(origin, format, ...)
#endif
class MySerialImpl : public Print {
public:
void begin(unsigned long baud) { logSerial.begin(baud); }
// Support boolean conversion for compatibility with code like:
// if (Serial) or while (!Serial)
operator bool() const { return logSerial; }
__attribute__((deprecated("Use LOG_* macro instead"))) size_t printf(const char* format, ...);
size_t write(uint8_t b) override;
size_t write(const uint8_t* buffer, size_t size) override;
void flush() override;
static MySerialImpl instance;
};
#define Serial MySerialImpl::instance

View File

@@ -1,6 +1,6 @@
#include "OpdsParser.h" #include "OpdsParser.h"
#include <HardwareSerial.h> #include <Logging.h>
#include <cstring> #include <cstring>
@@ -8,7 +8,7 @@ OpdsParser::OpdsParser() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
errorOccured = true; errorOccured = true;
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); LOG_DBG("OPDS", "Couldn't allocate memory for parser");
} }
} }
@@ -42,7 +42,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
void* const buf = XML_GetBuffer(parser, chunkSize); void* const buf = XML_GetBuffer(parser, chunkSize);
if (!buf) { if (!buf) {
errorOccured = true; errorOccured = true;
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis()); LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
return length; return length;
@@ -53,7 +53,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
errorOccured = true; errorOccured = true;
Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser))); XML_ErrorString(XML_GetErrorCode(parser)));
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
// Book icon: 48x48, 1-bit packed (MSB first)
// 0 = black, 1 = white (same format as Logo120.h)
static constexpr int BOOK_ICON_WIDTH = 48;
static constexpr int BOOK_ICON_HEIGHT = 48;
static const uint8_t BookIcon[] = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00,
0x00, 0x1f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1c, 0x00, 0x00, 0x01, 0x9f, 0xfc, 0x1f,
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1c, 0x00, 0x01,
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
0xfc, 0x1c, 0x00, 0x00, 0x1f, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f,
0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00,
0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
};

View File

@@ -0,0 +1,480 @@
#include "PlaceholderCoverGenerator.h"
#include <EpdFont.h>
#include <HalStorage.h>
#include <Logging.h>
#include <Utf8.h>
#include <algorithm>
#include <cstring>
#include <vector>
// Include the UI fonts directly for self-contained placeholder rendering.
// These are 1-bit bitmap fonts compiled from Ubuntu TTF.
#include "builtinFonts/ubuntu_10_regular.h"
#include "builtinFonts/ubuntu_12_bold.h"
// Book icon bitmap (48x48 1-bit, generated by scripts/generate_book_icon.py)
#include "BookIcon.h"
namespace {
// BMP writing helpers (same format as JpegToBmpConverter)
inline void write16(Print& out, const uint16_t value) {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
}
inline void write32(Print& out, const uint32_t value) {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
}
inline void write32Signed(Print& out, const int32_t value) {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
}
void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width + 31) / 32 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 62 + imageSize;
// BMP File Header (14 bytes)
bmpOut.write('B');
bmpOut.write('M');
write32(bmpOut, fileSize);
write32(bmpOut, 0); // Reserved
write32(bmpOut, 62); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER - 40 bytes)
write32(bmpOut, 40);
write32Signed(bmpOut, width);
write32Signed(bmpOut, -height); // Negative = top-down
write16(bmpOut, 1); // Color planes
write16(bmpOut, 1); // Bits per pixel
write32(bmpOut, 0); // BI_RGB
write32(bmpOut, imageSize);
write32(bmpOut, 2835); // xPixelsPerMeter
write32(bmpOut, 2835); // yPixelsPerMeter
write32(bmpOut, 2); // colorsUsed
write32(bmpOut, 2); // colorsImportant
// Palette: index 0 = black, index 1 = white
const uint8_t palette[8] = {
0x00, 0x00, 0x00, 0x00, // Black
0xFF, 0xFF, 0xFF, 0x00 // White
};
for (const uint8_t b : palette) {
bmpOut.write(b);
}
}
/// 1-bit pixel buffer that can render text, icons, and shapes, then write as BMP.
class PixelBuffer {
public:
PixelBuffer(int width, int height) : width(width), height(height) {
bytesPerRow = (width + 31) / 32 * 4;
bufferSize = bytesPerRow * height;
buffer = static_cast<uint8_t*>(malloc(bufferSize));
if (buffer) {
memset(buffer, 0xFF, bufferSize); // White background
}
}
~PixelBuffer() {
if (buffer) {
free(buffer);
}
}
bool isValid() const { return buffer != nullptr; }
/// Set a pixel to black.
void setBlack(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) return;
const int byteIndex = y * bytesPerRow + x / 8;
const uint8_t bitMask = 0x80 >> (x % 8);
buffer[byteIndex] &= ~bitMask;
}
/// Set a scaled "pixel" (scale x scale block) to black.
void setBlackScaled(int x, int y, int scale) {
for (int dy = 0; dy < scale; dy++) {
for (int dx = 0; dx < scale; dx++) {
setBlack(x + dx, y + dy);
}
}
}
/// Draw a filled rectangle in black.
void fillRect(int x, int y, int w, int h) {
for (int row = y; row < y + h && row < height; row++) {
for (int col = x; col < x + w && col < width; col++) {
setBlack(col, row);
}
}
}
/// Draw a rectangular border in black.
void drawBorder(int x, int y, int w, int h, int thickness) {
fillRect(x, y, w, thickness); // Top
fillRect(x, y + h - thickness, w, thickness); // Bottom
fillRect(x, y, thickness, h); // Left
fillRect(x + w - thickness, y, thickness, h); // Right
}
/// Draw a horizontal line in black with configurable thickness.
void drawHLine(int x, int y, int length, int thickness = 1) {
fillRect(x, y, length, thickness);
}
/// Render a single glyph at (cursorX, baselineY) with integer scaling. Returns advance in X (scaled).
int renderGlyph(const EpdFontData* font, uint32_t codepoint, int cursorX, int baselineY, int scale = 1) {
const EpdFont fontObj(font);
const EpdGlyph* glyph = fontObj.getGlyph(codepoint);
if (!glyph) {
glyph = fontObj.getGlyph(REPLACEMENT_GLYPH);
}
if (!glyph) {
return 0;
}
const uint8_t* bitmap = &font->bitmap[glyph->dataOffset];
const int glyphW = glyph->width;
const int glyphH = glyph->height;
for (int gy = 0; gy < glyphH; gy++) {
const int screenY = baselineY - glyph->top * scale + gy * scale;
for (int gx = 0; gx < glyphW; gx++) {
const int pixelPos = gy * glyphW + gx;
const int screenX = cursorX + glyph->left * scale + gx * scale;
bool isSet = false;
if (font->is2Bit) {
const uint8_t byte = bitmap[pixelPos / 4];
const uint8_t bitIndex = (3 - pixelPos % 4) * 2;
const uint8_t val = 3 - ((byte >> bitIndex) & 0x3);
isSet = (val < 3);
} else {
const uint8_t byte = bitmap[pixelPos / 8];
const uint8_t bitIndex = 7 - (pixelPos % 8);
isSet = ((byte >> bitIndex) & 1);
}
if (isSet) {
setBlackScaled(screenX, screenY, scale);
}
}
}
return glyph->advanceX * scale;
}
/// Render a UTF-8 string at (x, y) where y is the top of the text line, with integer scaling.
void drawText(const EpdFontData* font, int x, int y, const char* text, int scale = 1) {
const int baselineY = y + font->ascender * scale;
int cursorX = x;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
cursorX += renderGlyph(font, cp, cursorX, baselineY, scale);
}
}
/// Draw a 1-bit icon bitmap (MSB first, 0=black, 1=white) with integer scaling.
void drawIcon(const uint8_t* icon, int iconW, int iconH, int x, int y, int scale = 1) {
const int bytesPerIconRow = iconW / 8;
for (int iy = 0; iy < iconH; iy++) {
for (int ix = 0; ix < iconW; ix++) {
const int byteIdx = iy * bytesPerIconRow + ix / 8;
const uint8_t bitMask = 0x80 >> (ix % 8);
// In the icon data: 0 = black (drawn), 1 = white (skip)
if (!(icon[byteIdx] & bitMask)) {
const int sx = x + ix * scale;
const int sy = y + iy * scale;
setBlackScaled(sx, sy, scale);
}
}
}
}
/// Write the pixel buffer to a file as a 1-bit BMP.
bool writeBmp(Print& out) const {
if (!buffer) return false;
writeBmpHeader1bit(out, width, height);
out.write(buffer, bufferSize);
return true;
}
int getWidth() const { return width; }
int getHeight() const { return height; }
private:
int width;
int height;
int bytesPerRow;
size_t bufferSize;
uint8_t* buffer;
};
/// Measure the width of a UTF-8 string in pixels (at 1x scale).
int measureTextWidth(const EpdFontData* font, const char* text) {
const EpdFont fontObj(font);
int w = 0, h = 0;
fontObj.getTextDimensions(text, &w, &h);
return w;
}
/// Get the advance width of a single character.
int getCharAdvance(const EpdFontData* font, uint32_t cp) {
const EpdFont fontObj(font);
const EpdGlyph* glyph = fontObj.getGlyph(cp);
if (!glyph) return 0;
return glyph->advanceX;
}
/// Split a string into words (splitting on spaces).
std::vector<std::string> splitWords(const std::string& text) {
std::vector<std::string> words;
std::string current;
for (size_t i = 0; i < text.size(); i++) {
if (text[i] == ' ') {
if (!current.empty()) {
words.push_back(current);
current.clear();
}
} else {
current += text[i];
}
}
if (!current.empty()) {
words.push_back(current);
}
return words;
}
/// Word-wrap text into lines that fit within maxWidth pixels at the given scale.
std::vector<std::string> wrapText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
std::vector<std::string> lines;
const auto words = splitWords(text);
if (words.empty()) return lines;
const int spaceWidth = getCharAdvance(font, ' ') * scale;
std::string currentLine;
int currentWidth = 0;
for (const auto& word : words) {
const int wordWidth = measureTextWidth(font, word.c_str()) * scale;
if (currentLine.empty()) {
currentLine = word;
currentWidth = wordWidth;
} else if (currentWidth + spaceWidth + wordWidth <= maxWidth) {
currentLine += " " + word;
currentWidth += spaceWidth + wordWidth;
} else {
lines.push_back(currentLine);
currentLine = word;
currentWidth = wordWidth;
}
}
if (!currentLine.empty()) {
lines.push_back(currentLine);
}
return lines;
}
/// Truncate a string with "..." if it exceeds maxWidth pixels at the given scale.
std::string truncateText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
if (measureTextWidth(font, text.c_str()) * scale <= maxWidth) {
return text;
}
std::string truncated = text;
const char* ellipsis = "...";
const int ellipsisWidth = measureTextWidth(font, ellipsis) * scale;
while (!truncated.empty()) {
utf8RemoveLastChar(truncated);
if (measureTextWidth(font, truncated.c_str()) * scale + ellipsisWidth <= maxWidth) {
return truncated + ellipsis;
}
}
return ellipsis;
}
} // namespace
bool PlaceholderCoverGenerator::generate(const std::string& outputPath, const std::string& title,
const std::string& author, int width, int height) {
LOG_DBG("PHC", "Generating placeholder cover %dx%d: \"%s\" by \"%s\"", width, height, title.c_str(), author.c_str());
const EpdFontData* titleFont = &ubuntu_12_bold;
const EpdFontData* authorFont = &ubuntu_10_regular;
PixelBuffer buf(width, height);
if (!buf.isValid()) {
LOG_ERR("PHC", "Failed to allocate %dx%d pixel buffer (%d bytes)", width, height,
(width + 31) / 32 * 4 * height);
return false;
}
// Proportional layout constants based on cover dimensions.
// The device bezel covers ~2-3px on each edge, so we pad inward from the edge.
const int edgePadding = std::max(3, width / 48); // ~10px at 480w, ~3px at 136w
const int borderWidth = std::max(2, width / 96); // ~5px at 480w, ~2px at 136w
const int innerPadding = std::max(4, width / 32); // ~15px at 480w, ~4px at 136w
// Text scaling: 2x for full-size covers, 1x for thumbnails
const int titleScale = (height >= 600) ? 2 : 1;
const int authorScale = (height >= 600) ? 2 : 1; // Author also larger on full covers
// Icon: 2x for full cover, 1x for medium thumb, skip for small
const int iconScale = (height >= 600) ? 2 : (height >= 350 ? 1 : 0);
// Draw border inset from edge
buf.drawBorder(edgePadding, edgePadding, width - 2 * edgePadding, height - 2 * edgePadding, borderWidth);
// Content area (inside border + inner padding)
const int contentX = edgePadding + borderWidth + innerPadding;
const int contentY = edgePadding + borderWidth + innerPadding;
const int contentW = width - 2 * contentX;
const int contentH = height - 2 * contentY;
if (contentW <= 0 || contentH <= 0) {
LOG_ERR("PHC", "Cover too small for content (%dx%d)", width, height);
FsFile file;
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
return false;
}
buf.writeBmp(file);
file.close();
return true;
}
// --- Layout zones ---
// Title zone: top 2/3 of content area (icon + title)
// Author zone: bottom 1/3 of content area
const int titleZoneH = contentH * 2 / 3;
const int authorZoneH = contentH - titleZoneH;
const int authorZoneY = contentY + titleZoneH;
// --- Separator line at the zone boundary ---
const int separatorWidth = contentW / 3;
const int separatorX = contentX + (contentW - separatorWidth) / 2;
buf.drawHLine(separatorX, authorZoneY, separatorWidth);
// --- Icon dimensions (needed for title text wrapping) ---
const int iconW = (iconScale > 0) ? BOOK_ICON_WIDTH * iconScale : 0;
const int iconGap = (iconScale > 0) ? std::max(8, width / 40) : 0; // Gap between icon and title text
const int titleTextW = contentW - iconW - iconGap; // Title wraps in narrower area beside icon
// --- Prepare title text (wraps within the area to the right of the icon) ---
const std::string displayTitle = title.empty() ? "Untitled" : title;
auto titleLines = wrapText(titleFont, displayTitle, titleTextW, titleScale);
constexpr int MAX_TITLE_LINES = 5;
if (static_cast<int>(titleLines.size()) > MAX_TITLE_LINES) {
titleLines.resize(MAX_TITLE_LINES);
titleLines.back() = truncateText(titleFont, titleLines.back(), titleTextW, titleScale);
}
// --- Prepare author text (multi-line, max 3 lines) ---
std::vector<std::string> authorLines;
if (!author.empty()) {
authorLines = wrapText(authorFont, author, contentW, authorScale);
constexpr int MAX_AUTHOR_LINES = 3;
if (static_cast<int>(authorLines.size()) > MAX_AUTHOR_LINES) {
authorLines.resize(MAX_AUTHOR_LINES);
authorLines.back() = truncateText(authorFont, authorLines.back(), contentW, authorScale);
}
}
// --- Calculate title zone layout (icon LEFT of title) ---
// Tighter line spacing so 2-3 title lines fit within the icon height
const int titleLineH = titleFont->advanceY * titleScale * 3 / 4;
const int iconH = (iconScale > 0) ? BOOK_ICON_HEIGHT * iconScale : 0;
const int numTitleLines = static_cast<int>(titleLines.size());
// Visual height: distance from top of first line to bottom of last line's glyphs.
// Use ascender (not full advanceY) for the last line since trailing line-gap isn't visible.
const int titleVisualH = (numTitleLines > 0)
? (numTitleLines - 1) * titleLineH + titleFont->ascender * titleScale
: 0;
const int titleBlockH = std::max(iconH, titleVisualH); // Taller of icon or text
int titleStartY = contentY + (titleZoneH - titleBlockH) / 2;
if (titleStartY < contentY) {
titleStartY = contentY;
}
// If title fits within icon height, center it vertically against the icon.
// Otherwise top-align so extra lines overflow below.
const int iconY = titleStartY;
const int titleTextY = (iconH > 0 && titleVisualH <= iconH)
? titleStartY + (iconH - titleVisualH) / 2
: titleStartY;
// --- Horizontal centering: measure the widest title line, then center icon+gap+text block ---
int maxTitleLineW = 0;
for (const auto& line : titleLines) {
const int w = measureTextWidth(titleFont, line.c_str()) * titleScale;
if (w > maxTitleLineW) maxTitleLineW = w;
}
const int titleBlockW = iconW + iconGap + maxTitleLineW;
const int titleBlockX = contentX + (contentW - titleBlockW) / 2;
// --- Draw icon ---
if (iconScale > 0) {
buf.drawIcon(BookIcon, BOOK_ICON_WIDTH, BOOK_ICON_HEIGHT, titleBlockX, iconY, iconScale);
}
// --- Draw title lines (to the right of the icon) ---
const int titleTextX = titleBlockX + iconW + iconGap;
int currentY = titleTextY;
for (const auto& line : titleLines) {
buf.drawText(titleFont, titleTextX, currentY, line.c_str(), titleScale);
currentY += titleLineH;
}
// --- Draw author lines (centered vertically in bottom 1/3, centered horizontally) ---
if (!authorLines.empty()) {
const int authorLineH = authorFont->advanceY * authorScale;
const int authorBlockH = static_cast<int>(authorLines.size()) * authorLineH;
int authorStartY = authorZoneY + (authorZoneH - authorBlockH) / 2;
if (authorStartY < authorZoneY + 4) {
authorStartY = authorZoneY + 4; // Small gap below separator
}
for (const auto& line : authorLines) {
const int lineWidth = measureTextWidth(authorFont, line.c_str()) * authorScale;
const int lineX = contentX + (contentW - lineWidth) / 2;
buf.drawText(authorFont, lineX, authorStartY, line.c_str(), authorScale);
authorStartY += authorLineH;
}
}
// --- Write to file ---
FsFile file;
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
LOG_ERR("PHC", "Failed to open output file: %s", outputPath.c_str());
return false;
}
const bool success = buf.writeBmp(file);
file.close();
if (success) {
LOG_DBG("PHC", "Placeholder cover written to %s", outputPath.c_str());
} else {
LOG_ERR("PHC", "Failed to write placeholder BMP");
Storage.remove(outputPath.c_str());
}
return success;
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <string>
/// Generates simple 1-bit BMP placeholder covers with title/author text
/// for books that have no embedded cover image.
class PlaceholderCoverGenerator {
public:
/// Generate a placeholder cover BMP with title and author text.
/// The BMP is written to outputPath as a 1-bit black-and-white image.
/// Returns true if the file was written successfully.
static bool generate(const std::string& outputPath, const std::string& title, const std::string& author, int width,
int height);
};

View File

@@ -0,0 +1,858 @@
#include "PngToBmpConverter.h"
#include <HalStorage.h>
#include <Logging.h>
#include <miniz.h>
#include <cstdio>
#include <cstring>
#include "BitmapHelpers.h"
// ============================================================================
// IMAGE PROCESSING OPTIONS - Same as JpegToBmpConverter for consistency
// ============================================================================
constexpr bool USE_8BIT_OUTPUT = false;
constexpr bool USE_ATKINSON = true;
constexpr bool USE_FLOYD_STEINBERG = false;
constexpr bool USE_PRESCALE = true;
constexpr int TARGET_MAX_WIDTH = 480;
constexpr int TARGET_MAX_HEIGHT = 800;
// ============================================================================
// PNG constants
static constexpr uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10};
// PNG color types
enum PngColorType : uint8_t {
PNG_COLOR_GRAYSCALE = 0,
PNG_COLOR_RGB = 2,
PNG_COLOR_PALETTE = 3,
PNG_COLOR_GRAYSCALE_ALPHA = 4,
PNG_COLOR_RGBA = 6,
};
// PNG filter types
enum PngFilter : uint8_t {
PNG_FILTER_NONE = 0,
PNG_FILTER_SUB = 1,
PNG_FILTER_UP = 2,
PNG_FILTER_AVERAGE = 3,
PNG_FILTER_PAETH = 4,
};
// Read a big-endian 32-bit value from file
static bool readBE32(FsFile& file, uint32_t& value) {
uint8_t buf[4];
if (file.read(buf, 4) != 4) return false;
value = (static_cast<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
(static_cast<uint32_t>(buf[2]) << 8) | buf[3];
return true;
}
// BMP writing helpers (same as JpegToBmpConverter)
inline void write16(Print& out, const uint16_t value) {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
}
inline void write32(Print& out, const uint32_t value) {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
}
inline void write32Signed(Print& out, const int32_t value) {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
}
static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width + 3) / 4 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t paletteSize = 256 * 4;
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
bmpOut.write('B');
bmpOut.write('M');
write32(bmpOut, fileSize);
write32(bmpOut, 0);
write32(bmpOut, 14 + 40 + paletteSize);
write32(bmpOut, 40);
write32Signed(bmpOut, width);
write32Signed(bmpOut, -height);
write16(bmpOut, 1);
write16(bmpOut, 8);
write32(bmpOut, 0);
write32(bmpOut, imageSize);
write32(bmpOut, 2835);
write32(bmpOut, 2835);
write32(bmpOut, 256);
write32(bmpOut, 256);
for (int i = 0; i < 256; i++) {
bmpOut.write(static_cast<uint8_t>(i));
bmpOut.write(static_cast<uint8_t>(i));
bmpOut.write(static_cast<uint8_t>(i));
bmpOut.write(static_cast<uint8_t>(0));
}
}
static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width + 31) / 32 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 62 + imageSize;
bmpOut.write('B');
bmpOut.write('M');
write32(bmpOut, fileSize);
write32(bmpOut, 0);
write32(bmpOut, 62);
write32(bmpOut, 40);
write32Signed(bmpOut, width);
write32Signed(bmpOut, -height);
write16(bmpOut, 1);
write16(bmpOut, 1);
write32(bmpOut, 0);
write32(bmpOut, imageSize);
write32(bmpOut, 2835);
write32(bmpOut, 2835);
write32(bmpOut, 2);
write32(bmpOut, 2);
uint8_t palette[8] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
for (const uint8_t i : palette) {
bmpOut.write(i);
}
}
static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width * 2 + 31) / 32 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 70 + imageSize;
bmpOut.write('B');
bmpOut.write('M');
write32(bmpOut, fileSize);
write32(bmpOut, 0);
write32(bmpOut, 70);
write32(bmpOut, 40);
write32Signed(bmpOut, width);
write32Signed(bmpOut, -height);
write16(bmpOut, 1);
write16(bmpOut, 2);
write32(bmpOut, 0);
write32(bmpOut, imageSize);
write32(bmpOut, 2835);
write32(bmpOut, 2835);
write32(bmpOut, 4);
write32(bmpOut, 4);
uint8_t palette[16] = {0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x00,
0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
for (const uint8_t i : palette) {
bmpOut.write(i);
}
}
// Paeth predictor function per PNG spec
static inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) {
int p = static_cast<int>(a) + b - c;
int pa = p > a ? p - a : a - p;
int pb = p > b ? p - b : b - p;
int pc = p > c ? p - c : c - p;
if (pa <= pb && pa <= pc) return a;
if (pb <= pc) return b;
return c;
}
// Context for streaming PNG decompression
struct PngDecodeContext {
FsFile& file;
// PNG image properties
uint32_t width;
uint32_t height;
uint8_t bitDepth;
uint8_t colorType;
uint8_t bytesPerPixel; // after expanding sub-byte depths
uint32_t rawRowBytes; // bytes per raw row (without filter byte)
// Scanline buffers
uint8_t* currentRow; // current defiltered scanline
uint8_t* previousRow; // previous defiltered scanline
// zlib decompression state
mz_stream zstream;
bool zstreamInitialized;
// Chunk reading state
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
bool idatFinished; // no more IDAT chunks
// File read buffer for feeding zlib
uint8_t readBuf[2048];
// Palette for indexed color (type 3)
uint8_t palette[256 * 3];
int paletteSize;
};
// Read the next IDAT chunk header, skipping non-IDAT chunks
// Returns true if an IDAT chunk was found
static bool findNextIdatChunk(PngDecodeContext& ctx) {
while (true) {
uint32_t chunkLen;
if (!readBE32(ctx.file, chunkLen)) return false;
uint8_t chunkType[4];
if (ctx.file.read(chunkType, 4) != 4) return false;
if (memcmp(chunkType, "IDAT", 4) == 0) {
ctx.chunkBytesRemaining = chunkLen;
return true;
}
// Skip this chunk's data + 4-byte CRC
// Use seek to skip efficiently
if (!ctx.file.seekCur(chunkLen + 4)) return false;
// If we hit IEND, there are no more chunks
if (memcmp(chunkType, "IEND", 4) == 0) {
return false;
}
}
}
// Feed compressed data to zlib from IDAT chunks
// Returns number of bytes made available in zstream, or -1 on error
static int feedZlibInput(PngDecodeContext& ctx) {
if (ctx.idatFinished) return 0;
// If current IDAT chunk is exhausted, skip its CRC and find next
while (ctx.chunkBytesRemaining == 0) {
// Skip 4-byte CRC of previous IDAT
if (!ctx.file.seekCur(4)) return -1;
if (!findNextIdatChunk(ctx)) {
ctx.idatFinished = true;
return 0;
}
}
// Read from current IDAT chunk
size_t toRead = sizeof(ctx.readBuf);
if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining;
int bytesRead = ctx.file.read(ctx.readBuf, toRead);
if (bytesRead <= 0) return -1;
ctx.chunkBytesRemaining -= bytesRead;
ctx.zstream.next_in = ctx.readBuf;
ctx.zstream.avail_in = bytesRead;
return bytesRead;
}
// Decompress exactly 'needed' bytes into 'dest'
static bool decompressBytes(PngDecodeContext& ctx, uint8_t* dest, size_t needed) {
ctx.zstream.next_out = dest;
ctx.zstream.avail_out = needed;
while (ctx.zstream.avail_out > 0) {
if (ctx.zstream.avail_in == 0) {
int fed = feedZlibInput(ctx);
if (fed < 0) return false;
if (fed == 0) {
// Try one more inflate to flush
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
if (ctx.zstream.avail_out == 0) break;
return false;
}
}
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
if (ret != MZ_OK && ret != MZ_STREAM_END && ret != MZ_BUF_ERROR) {
LOG_ERR("PNG", "zlib inflate error: %d", ret);
return false;
}
if (ret == MZ_STREAM_END) break;
}
return ctx.zstream.avail_out == 0;
}
// Decode one scanline: decompress filter byte + raw bytes, then unfilter
static bool decodeScanline(PngDecodeContext& ctx) {
// Decompress filter byte
uint8_t filterType;
if (!decompressBytes(ctx, &filterType, 1)) return false;
// Decompress raw row data into currentRow
if (!decompressBytes(ctx, ctx.currentRow, ctx.rawRowBytes)) return false;
// Apply reverse filter
const int bpp = ctx.bytesPerPixel;
switch (filterType) {
case PNG_FILTER_NONE:
break;
case PNG_FILTER_SUB:
for (uint32_t i = bpp; i < ctx.rawRowBytes; i++) {
ctx.currentRow[i] += ctx.currentRow[i - bpp];
}
break;
case PNG_FILTER_UP:
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
ctx.currentRow[i] += ctx.previousRow[i];
}
break;
case PNG_FILTER_AVERAGE:
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
uint8_t b = ctx.previousRow[i];
ctx.currentRow[i] += (a + b) / 2;
}
break;
case PNG_FILTER_PAETH:
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
uint8_t b = ctx.previousRow[i];
uint8_t c = (i >= static_cast<uint32_t>(bpp)) ? ctx.previousRow[i - bpp] : 0;
ctx.currentRow[i] += paethPredictor(a, b, c);
}
break;
default:
LOG_ERR("PNG", "Unknown filter type: %d", filterType);
return false;
}
return true;
}
// Batch-convert an entire scanline to grayscale.
// Branches once on colorType/bitDepth, then runs a tight loop for the whole row.
static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) {
const uint8_t* src = ctx.currentRow;
const uint32_t w = ctx.width;
switch (ctx.colorType) {
case PNG_COLOR_GRAYSCALE:
if (ctx.bitDepth == 8) {
memcpy(grayRow, src, w);
} else if (ctx.bitDepth == 16) {
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
} else {
const int ppb = 8 / ctx.bitDepth;
const uint8_t mask = (1 << ctx.bitDepth) - 1;
for (uint32_t x = 0; x < w; x++) {
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
grayRow[x] = (src[x / ppb] >> shift & mask) * 255 / mask;
}
}
break;
case PNG_COLOR_RGB:
if (ctx.bitDepth == 8) {
// Fast path: most common EPUB cover format
for (uint32_t x = 0; x < w; x++) {
const uint8_t* p = src + x * 3;
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
}
} else {
for (uint32_t x = 0; x < w; x++) {
grayRow[x] = (src[x * 6] * 25 + src[x * 6 + 2] * 50 + src[x * 6 + 4] * 25) / 100;
}
}
break;
case PNG_COLOR_PALETTE: {
const int ppb = 8 / ctx.bitDepth;
const uint8_t mask = (1 << ctx.bitDepth) - 1;
const uint8_t* pal = ctx.palette;
const int palSize = ctx.paletteSize;
for (uint32_t x = 0; x < w; x++) {
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
uint8_t idx = (src[x / ppb] >> shift) & mask;
if (idx >= palSize) idx = 0;
grayRow[x] = (pal[idx * 3] * 25 + pal[idx * 3 + 1] * 50 + pal[idx * 3 + 2] * 25) / 100;
}
break;
}
case PNG_COLOR_GRAYSCALE_ALPHA:
if (ctx.bitDepth == 8) {
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
} else {
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 4];
}
break;
case PNG_COLOR_RGBA:
if (ctx.bitDepth == 8) {
for (uint32_t x = 0; x < w; x++) {
const uint8_t* p = src + x * 4;
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
}
} else {
for (uint32_t x = 0; x < w; x++) {
grayRow[x] = (src[x * 8] * 25 + src[x * 8 + 2] * 50 + src[x * 8 + 4] * 25) / 100;
}
}
break;
default:
memset(grayRow, 128, w);
break;
}
}
bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight,
bool oneBit, bool crop) {
LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
// Verify PNG signature
uint8_t sig[8];
if (pngFile.read(sig, 8) != 8 || memcmp(sig, PNG_SIGNATURE, 8) != 0) {
LOG_ERR("PNG", "Invalid PNG signature");
return false;
}
// Read IHDR chunk
uint32_t ihdrLen;
if (!readBE32(pngFile, ihdrLen)) return false;
uint8_t ihdrType[4];
if (pngFile.read(ihdrType, 4) != 4 || memcmp(ihdrType, "IHDR", 4) != 0) {
LOG_ERR("PNG", "Missing IHDR chunk");
return false;
}
uint32_t width, height;
if (!readBE32(pngFile, width) || !readBE32(pngFile, height)) return false;
uint8_t ihdrRest[5];
if (pngFile.read(ihdrRest, 5) != 5) return false;
uint8_t bitDepth = ihdrRest[0];
uint8_t colorType = ihdrRest[1];
uint8_t compression = ihdrRest[2];
uint8_t filter = ihdrRest[3];
uint8_t interlace = ihdrRest[4];
// Skip IHDR CRC
pngFile.seekCur(4);
LOG_DBG("PNG", "Image: %ux%u, depth=%u, color=%u, interlace=%u", width, height, bitDepth, colorType, interlace);
if (compression != 0 || filter != 0) {
LOG_ERR("PNG", "Unsupported compression/filter method");
return false;
}
if (interlace != 0) {
LOG_ERR("PNG", "Interlaced PNGs not supported");
return false;
}
// Safety limits
constexpr int MAX_IMAGE_WIDTH = 2048;
constexpr int MAX_IMAGE_HEIGHT = 3072;
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width == 0 || height == 0) {
LOG_ERR("PNG", "Image too large or zero (%ux%u)", width, height);
return false;
}
// Calculate bytes per pixel and raw row bytes
uint8_t bytesPerPixel;
uint32_t rawRowBytes;
switch (colorType) {
case PNG_COLOR_GRAYSCALE:
if (bitDepth == 16) {
bytesPerPixel = 2;
rawRowBytes = width * 2;
} else if (bitDepth == 8) {
bytesPerPixel = 1;
rawRowBytes = width;
} else {
// Sub-byte: 1, 2, or 4 bits
bytesPerPixel = 1;
rawRowBytes = (width * bitDepth + 7) / 8;
}
break;
case PNG_COLOR_RGB:
bytesPerPixel = (bitDepth == 16) ? 6 : 3;
rawRowBytes = width * bytesPerPixel;
break;
case PNG_COLOR_PALETTE:
bytesPerPixel = 1;
rawRowBytes = (width * bitDepth + 7) / 8;
break;
case PNG_COLOR_GRAYSCALE_ALPHA:
bytesPerPixel = (bitDepth == 16) ? 4 : 2;
rawRowBytes = width * bytesPerPixel;
break;
case PNG_COLOR_RGBA:
bytesPerPixel = (bitDepth == 16) ? 8 : 4;
rawRowBytes = width * bytesPerPixel;
break;
default:
LOG_ERR("PNG", "Unsupported color type: %d", colorType);
return false;
}
// Validate raw row bytes won't cause memory issues
if (rawRowBytes > 16384) {
LOG_ERR("PNG", "Row too large: %u bytes", rawRowBytes);
return false;
}
// Initialize decode context
PngDecodeContext ctx = {.file = pngFile,
.width = width,
.height = height,
.bitDepth = bitDepth,
.colorType = colorType,
.bytesPerPixel = bytesPerPixel,
.rawRowBytes = rawRowBytes,
.currentRow = nullptr,
.previousRow = nullptr,
.zstream = {},
.zstreamInitialized = false,
.chunkBytesRemaining = 0,
.idatFinished = false,
.readBuf = {},
.palette = {},
.paletteSize = 0};
// Allocate scanline buffers
ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes));
ctx.previousRow = static_cast<uint8_t*>(calloc(rawRowBytes, 1));
if (!ctx.currentRow || !ctx.previousRow) {
LOG_ERR("PNG", "Failed to allocate scanline buffers (%u bytes each)", rawRowBytes);
free(ctx.currentRow);
free(ctx.previousRow);
return false;
}
// Scan for PLTE chunk (palette) and first IDAT chunk
// We need to read chunks until we find IDAT, collecting PLTE along the way
bool foundIdat = false;
while (!foundIdat) {
uint32_t chunkLen;
if (!readBE32(pngFile, chunkLen)) break;
uint8_t chunkType[4];
if (pngFile.read(chunkType, 4) != 4) break;
if (memcmp(chunkType, "PLTE", 4) == 0) {
int entries = chunkLen / 3;
if (entries > 256) entries = 256;
ctx.paletteSize = entries;
size_t palBytes = entries * 3;
pngFile.read(ctx.palette, palBytes);
// Skip any remaining palette data
if (chunkLen > palBytes) pngFile.seekCur(chunkLen - palBytes);
pngFile.seekCur(4); // CRC
} else if (memcmp(chunkType, "IDAT", 4) == 0) {
ctx.chunkBytesRemaining = chunkLen;
foundIdat = true;
} else if (memcmp(chunkType, "IEND", 4) == 0) {
break;
} else {
// Skip unknown chunk
pngFile.seekCur(chunkLen + 4);
}
}
if (!foundIdat) {
LOG_ERR("PNG", "No IDAT chunk found");
free(ctx.currentRow);
free(ctx.previousRow);
return false;
}
// Initialize zlib decompression
memset(&ctx.zstream, 0, sizeof(ctx.zstream));
if (mz_inflateInit(&ctx.zstream) != MZ_OK) {
LOG_ERR("PNG", "Failed to initialize zlib");
free(ctx.currentRow);
free(ctx.previousRow);
return false;
}
ctx.zstreamInitialized = true;
// Calculate output dimensions (same logic as JpegToBmpConverter)
int outWidth = width;
int outHeight = height;
uint32_t scaleX_fp = 65536;
uint32_t scaleY_fp = 65536;
bool needsScaling = false;
if (targetWidth > 0 && targetHeight > 0 &&
(static_cast<int>(width) > targetWidth || static_cast<int>(height) > targetHeight)) {
const float scaleToFitWidth = static_cast<float>(targetWidth) / width;
const float scaleToFitHeight = static_cast<float>(targetHeight) / height;
float scale = 1.0;
if (crop) {
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
} else {
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
}
outWidth = static_cast<int>(width * scale);
outHeight = static_cast<int>(height * scale);
if (outWidth < 1) outWidth = 1;
if (outHeight < 1) outHeight = 1;
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth;
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight;
needsScaling = true;
LOG_DBG("PNG", "Pre-scaling %ux%u -> %dx%d (fit to %dx%d)", width, height, outWidth, outHeight, targetWidth,
targetHeight);
}
// Write BMP header
int bytesPerRow;
if (USE_8BIT_OUTPUT && !oneBit) {
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
bytesPerRow = (outWidth + 3) / 4 * 4;
} else if (oneBit) {
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
bytesPerRow = (outWidth + 31) / 32 * 4;
} else {
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
}
// Allocate BMP row buffer
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
if (!rowBuffer) {
LOG_ERR("PNG", "Failed to allocate row buffer");
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow);
free(ctx.previousRow);
return false;
}
// Create ditherers (same as JpegToBmpConverter)
AtkinsonDitherer* atkinsonDitherer = nullptr;
FloydSteinbergDitherer* fsDitherer = nullptr;
Atkinson1BitDitherer* atkinson1BitDitherer = nullptr;
if (oneBit) {
atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth);
} else if (!USE_8BIT_OUTPUT) {
if (USE_ATKINSON) {
atkinsonDitherer = new AtkinsonDitherer(outWidth);
} else if (USE_FLOYD_STEINBERG) {
fsDitherer = new FloydSteinbergDitherer(outWidth);
}
}
// Scaling accumulators
uint32_t* rowAccum = nullptr;
uint16_t* rowCount = nullptr;
int currentOutY = 0;
uint32_t nextOutY_srcStart = 0;
if (needsScaling) {
rowAccum = new uint32_t[outWidth]();
rowCount = new uint16_t[outWidth]();
nextOutY_srcStart = scaleY_fp;
}
// Allocate grayscale row buffer - batch-convert each scanline to avoid
// per-pixel getPixelGray() switch overhead in the hot loops
auto* grayRow = static_cast<uint8_t*>(malloc(width));
if (!grayRow) {
LOG_ERR("PNG", "Failed to allocate grayscale row buffer");
delete[] rowAccum;
delete[] rowCount;
delete atkinsonDitherer;
delete fsDitherer;
delete atkinson1BitDitherer;
free(rowBuffer);
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow);
free(ctx.previousRow);
return false;
}
bool success = true;
// Process each scanline
for (uint32_t y = 0; y < height; y++) {
// Decode one scanline
if (!decodeScanline(ctx)) {
LOG_ERR("PNG", "Failed to decode scanline %u", y);
success = false;
break;
}
// Batch-convert entire scanline to grayscale (one branch, tight loop)
convertScanlineToGray(ctx, grayRow);
if (!needsScaling) {
// Direct output (no scaling)
memset(rowBuffer, 0, bytesPerRow);
if (USE_8BIT_OUTPUT && !oneBit) {
for (int x = 0; x < outWidth; x++) {
rowBuffer[x] = adjustPixel(grayRow[x]);
}
} else if (oneBit) {
for (int x = 0; x < outWidth; x++) {
const uint8_t bit =
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(grayRow[x], x) : quantize1bit(grayRow[x], x, y);
const int byteIndex = x / 8;
const int bitOffset = 7 - (x % 8);
rowBuffer[byteIndex] |= (bit << bitOffset);
}
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
} else {
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = adjustPixel(grayRow[x]);
uint8_t twoBit;
if (atkinsonDitherer) {
twoBit = atkinsonDitherer->processPixel(gray, x);
} else if (fsDitherer) {
twoBit = fsDitherer->processPixel(gray, x);
} else {
twoBit = quantize(gray, x, y);
}
const int byteIndex = (x * 2) / 8;
const int bitOffset = 6 - ((x * 2) % 8);
rowBuffer[byteIndex] |= (twoBit << bitOffset);
}
if (atkinsonDitherer)
atkinsonDitherer->nextRow();
else if (fsDitherer)
fsDitherer->nextRow();
}
bmpOut.write(rowBuffer, bytesPerRow);
} else {
// Area-averaging scaling (same as JpegToBmpConverter)
for (int outX = 0; outX < outWidth; outX++) {
const int srcXStart = (static_cast<uint32_t>(outX) * scaleX_fp) >> 16;
const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16;
int sum = 0;
int count = 0;
for (int srcX = srcXStart; srcX < srcXEnd && srcX < static_cast<int>(width); srcX++) {
sum += grayRow[srcX];
count++;
}
if (count == 0 && srcXStart < static_cast<int>(width)) {
sum = grayRow[srcXStart];
count = 1;
}
rowAccum[outX] += sum;
rowCount[outX] += count;
}
// Check if we've crossed into the next output row
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
memset(rowBuffer, 0, bytesPerRow);
if (USE_8BIT_OUTPUT && !oneBit) {
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
rowBuffer[x] = adjustPixel(gray);
}
} else if (oneBit) {
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
const uint8_t bit =
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, currentOutY);
const int byteIndex = x / 8;
const int bitOffset = 7 - (x % 8);
rowBuffer[byteIndex] |= (bit << bitOffset);
}
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
} else {
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
uint8_t twoBit;
if (atkinsonDitherer) {
twoBit = atkinsonDitherer->processPixel(gray, x);
} else if (fsDitherer) {
twoBit = fsDitherer->processPixel(gray, x);
} else {
twoBit = quantize(gray, x, currentOutY);
}
const int byteIndex = (x * 2) / 8;
const int bitOffset = 6 - ((x * 2) % 8);
rowBuffer[byteIndex] |= (twoBit << bitOffset);
}
if (atkinsonDitherer)
atkinsonDitherer->nextRow();
else if (fsDitherer)
fsDitherer->nextRow();
}
bmpOut.write(rowBuffer, bytesPerRow);
currentOutY++;
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
memset(rowCount, 0, outWidth * sizeof(uint16_t));
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
}
}
// Swap current/previous row buffers
uint8_t* temp = ctx.previousRow;
ctx.previousRow = ctx.currentRow;
ctx.currentRow = temp;
}
// Clean up
free(grayRow);
delete[] rowAccum;
delete[] rowCount;
delete atkinsonDitherer;
delete fsDitherer;
delete atkinson1BitDitherer;
free(rowBuffer);
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow);
free(ctx.previousRow);
if (success) {
LOG_DBG("PNG", "Successfully converted PNG to BMP");
}
return success;
}
bool PngToBmpConverter::pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop) {
return pngFileToBmpStreamInternal(pngFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop);
}
bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
int targetMaxHeight) {
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false);
}
bool PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
int targetMaxHeight) {
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
}

View File

@@ -0,0 +1,14 @@
#pragma once
class FsFile;
class Print;
class PngToBmpConverter {
static bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit,
bool crop = true);
public:
static bool pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop = true);
static bool pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
};

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#include <SdFat.h> #include <HalStorage.h>
#include <iostream> #include <iostream>

View File

@@ -2,6 +2,7 @@
#include <FsHelpers.h> #include <FsHelpers.h>
#include <JpegToBmpConverter.h> #include <JpegToBmpConverter.h>
#include <Logging.h>
Txt::Txt(std::string path, std::string cacheBasePath) Txt::Txt(std::string path, std::string cacheBasePath)
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) { : filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
@@ -15,14 +16,14 @@ bool Txt::load() {
return true; return true;
} }
if (!SdMan.exists(filepath.c_str())) { if (!Storage.exists(filepath.c_str())) {
Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str()); LOG_ERR("TXT", "File does not exist: %s", filepath.c_str());
return false; return false;
} }
FsFile file; FsFile file;
if (!SdMan.openFileForRead("TXT", filepath, file)) { if (!Storage.openFileForRead("TXT", filepath, file)) {
Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str()); LOG_ERR("TXT", "Failed to open file: %s", filepath.c_str());
return false; return false;
} }
@@ -30,7 +31,7 @@ bool Txt::load() {
file.close(); file.close();
loaded = true; loaded = true;
Serial.printf("[%lu] [TXT] Loaded TXT file: %s (%zu bytes)\n", millis(), filepath.c_str(), fileSize); LOG_DBG("TXT", "Loaded TXT file: %s (%zu bytes)", filepath.c_str(), fileSize);
return true; return true;
} }
@@ -48,11 +49,11 @@ std::string Txt::getTitle() const {
} }
void Txt::setupCacheDir() const { void Txt::setupCacheDir() const {
if (!SdMan.exists(cacheBasePath.c_str())) { if (!Storage.exists(cacheBasePath.c_str())) {
SdMan.mkdir(cacheBasePath.c_str()); Storage.mkdir(cacheBasePath.c_str());
} }
if (!SdMan.exists(cachePath.c_str())) { if (!Storage.exists(cachePath.c_str())) {
SdMan.mkdir(cachePath.c_str()); Storage.mkdir(cachePath.c_str());
} }
} }
@@ -73,8 +74,8 @@ std::string Txt::findCoverImage() const {
// First priority: look for image with same name as txt file (e.g., mybook.jpg) // First priority: look for image with same name as txt file (e.g., mybook.jpg)
for (const auto& ext : extensions) { for (const auto& ext : extensions) {
std::string coverPath = folder + "/" + baseName + ext; std::string coverPath = folder + "/" + baseName + ext;
if (SdMan.exists(coverPath.c_str())) { if (Storage.exists(coverPath.c_str())) {
Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str()); LOG_DBG("TXT", "Found matching cover image: %s", coverPath.c_str());
return coverPath; return coverPath;
} }
} }
@@ -84,8 +85,8 @@ std::string Txt::findCoverImage() const {
for (const auto& name : coverNames) { for (const auto& name : coverNames) {
for (const auto& ext : extensions) { for (const auto& ext : extensions) {
std::string coverPath = folder + "/" + std::string(name) + ext; std::string coverPath = folder + "/" + std::string(name) + ext;
if (SdMan.exists(coverPath.c_str())) { if (Storage.exists(coverPath.c_str())) {
Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str()); LOG_DBG("TXT", "Found fallback cover image: %s", coverPath.c_str());
return coverPath; return coverPath;
} }
} }
@@ -96,15 +97,18 @@ std::string Txt::findCoverImage() const {
std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
std::string Txt::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
std::string Txt::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
bool Txt::generateCoverBmp() const { bool Txt::generateCoverBmp() const {
// Already generated, return true // Already generated, return true
if (SdMan.exists(getCoverBmpPath().c_str())) { if (Storage.exists(getCoverBmpPath().c_str())) {
return true; return true;
} }
std::string coverImagePath = findCoverImage(); std::string coverImagePath = findCoverImage();
if (coverImagePath.empty()) { if (coverImagePath.empty()) {
Serial.printf("[%lu] [TXT] No cover image found for TXT file\n", millis()); LOG_DBG("TXT", "No cover image found for TXT file");
return false; return false;
} }
@@ -120,12 +124,12 @@ bool Txt::generateCoverBmp() const {
if (isBmp) { if (isBmp) {
// Copy BMP file to cache // Copy BMP file to cache
Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis()); LOG_DBG("TXT", "Copying BMP cover image to cache");
FsFile src, dst; FsFile src, dst;
if (!SdMan.openFileForRead("TXT", coverImagePath, src)) { if (!Storage.openFileForRead("TXT", coverImagePath, src)) {
return false; return false;
} }
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) { if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
src.close(); src.close();
return false; return false;
} }
@@ -136,18 +140,18 @@ bool Txt::generateCoverBmp() const {
} }
src.close(); src.close();
dst.close(); dst.close();
Serial.printf("[%lu] [TXT] Copied BMP cover to cache\n", millis()); LOG_DBG("TXT", "Copied BMP cover to cache");
return true; return true;
} }
if (isJpg) { if (isJpg) {
// Convert JPG/JPEG to BMP (same approach as Epub) // Convert JPG/JPEG to BMP (same approach as Epub)
Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis()); LOG_DBG("TXT", "Generating BMP from JPG cover image");
FsFile coverJpg, coverBmp; FsFile coverJpg, coverBmp;
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) { if (!Storage.openFileForRead("TXT", coverImagePath, coverJpg)) {
return false; return false;
} }
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) { if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
coverJpg.close(); coverJpg.close();
return false; return false;
} }
@@ -156,16 +160,16 @@ bool Txt::generateCoverBmp() const {
coverBmp.close(); coverBmp.close();
if (!success) { if (!success) {
Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis()); LOG_ERR("TXT", "Failed to generate BMP from JPG cover image");
SdMan.remove(getCoverBmpPath().c_str()); Storage.remove(getCoverBmpPath().c_str());
} else { } else {
Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis()); LOG_DBG("TXT", "Generated BMP from JPG cover image");
} }
return success; return success;
} }
// PNG files are not supported (would need a PNG decoder) // PNG files are not supported (would need a PNG decoder)
Serial.printf("[%lu] [TXT] Cover image format not supported (only BMP/JPG/JPEG)\n", millis()); LOG_ERR("TXT", "Cover image format not supported (only BMP/JPG/JPEG)");
return false; return false;
} }
@@ -175,7 +179,7 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
} }
FsFile file; FsFile file;
if (!SdMan.openFileForRead("TXT", filepath, file)) { if (!Storage.openFileForRead("TXT", filepath, file)) {
return false; return false;
} }

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <SDCardManager.h> #include <HalStorage.h>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -28,6 +28,10 @@ class Txt {
[[nodiscard]] bool generateCoverBmp() const; [[nodiscard]] bool generateCoverBmp() const;
[[nodiscard]] std::string findCoverImage() const; [[nodiscard]] std::string findCoverImage() const;
// Thumbnail paths (matching Epub/Xtc pattern for home screen covers)
[[nodiscard]] std::string getThumbBmpPath() const;
[[nodiscard]] std::string getThumbBmpPath(int height) const;
// Read content from file // Read content from file
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const; [[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
}; };

View File

@@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
return cp; return cp;
} }
size_t utf8RemoveLastChar(std::string& str) {
if (str.empty()) return 0;
size_t pos = str.size() - 1;
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
--pos;
}
str.resize(pos);
return pos;
}
// Truncate string by removing N UTF-8 characters from the end
void utf8TruncateChars(std::string& str, const size_t numChars) {
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
utf8RemoveLastChar(str);
}
}

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