Compare commits
135
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a86a644adc | ||
|
|
02398c8a96 | ||
|
|
ba92cb902e | ||
|
|
4d19d38152 | ||
|
|
302c0771dd | ||
|
|
c1a396c1ba | ||
|
|
078d9ef535 | ||
|
|
17e4230068 | ||
|
|
34e7ed249b | ||
|
|
c5787d1cc1 | ||
|
|
2a19eb9e0d | ||
|
|
fae3423dec | ||
|
|
932a472835 | ||
|
|
552b2683e6 | ||
|
|
bfa1d706b1 | ||
|
|
73ab9ab20d | ||
|
|
3a97f3e63e | ||
|
|
39ea4b045f | ||
|
|
444d87de82 | ||
|
|
859f6cb0d5 | ||
|
|
d8139e9605 | ||
|
|
b596f21e61 | ||
|
|
5fbc657aeb | ||
|
|
bb06219c9c | ||
|
|
5202bbf911 | ||
|
|
e0253a8664 | ||
|
|
e142e88ce7 | ||
|
|
6add0b91bc | ||
|
|
287457f7dd | ||
|
|
3e627112f6 | ||
|
|
1f5669a08a | ||
|
|
0dc92c142c | ||
|
|
4b34a576eb | ||
|
|
4fb843ef92 | ||
|
|
cecdbefa0e | ||
|
|
4c5fd653c0 | ||
|
|
05c1e9aa46 | ||
|
|
f3da5e4f06 | ||
|
|
a43061305a | ||
|
|
7ec07b4d9d | ||
|
|
c2c1badc7e | ||
|
|
5776911af0 | ||
|
|
3c5c5e8aa7 | ||
|
|
6f5c5a0900 | ||
|
|
f2b00f6b93 | ||
|
|
44ff313740 | ||
|
|
6f0707e83c | ||
|
|
7b6df60a54 | ||
|
|
1c13913713 | ||
|
|
613371b716 | ||
|
|
0d7a84e3c5 | ||
|
|
4c93950771 | ||
|
|
1d01eebf55 | ||
|
|
9bf6e5f43c | ||
|
|
ca1b833126 | ||
|
|
3e1dd31e53 | ||
|
|
685d4e88f9 | ||
|
|
79b4d63ee2 | ||
|
|
ad3138983b | ||
|
|
793685e76b | ||
|
|
551d29744a | ||
|
|
74a0969cc6 | ||
|
|
06aa1ac2f7 | ||
|
|
57447a56a6 | ||
|
|
5c86bfe1fd | ||
|
|
4cdefb137c | ||
|
|
e7f3406704 | ||
|
|
6f08a88b12 | ||
|
|
c463da994e | ||
|
|
e4d3981469 | ||
|
|
c9d27826f9 | ||
|
|
13bd00bf9d | ||
|
|
fef39da23c | ||
|
|
fd43ca2fe1 | ||
|
|
28255061fb | ||
|
|
43ff1b6644 | ||
|
|
2754a5ff01 | ||
|
|
6edeed73c5 | ||
|
|
8ac302018c | ||
|
|
a11517a62d | ||
|
|
cbaa498ccc | ||
|
|
ebebc6f202 | ||
|
|
970b2c6ca1 | ||
|
|
b6ce599b20 | ||
|
|
f54eab2725 | ||
|
|
0d4c9ab91b | ||
|
|
0daa9db243 | ||
|
|
a2f2eea79e | ||
|
|
86a9b9c4a2 | ||
|
|
ba44978ac4 | ||
|
|
555f76da88 | ||
|
|
0a57c0a5a7 | ||
|
|
a09aef0889 | ||
|
|
8626d69f46 | ||
|
|
fc89e57e69 | ||
|
|
487613b082 | ||
|
|
8d5b119644 | ||
|
|
988652513a | ||
|
|
7271c00d35 | ||
|
|
28d0d6f5d2 | ||
|
|
568831f232 | ||
|
|
d6f5be6b7a | ||
|
|
d30fde2f4d | ||
|
|
6305777b22 | ||
|
|
9ab0b0bfb7 | ||
|
|
6e8dbd7f23 | ||
|
|
c4b1d9644a | ||
|
|
f6e59aab72 | ||
|
|
9ad2da0950 | ||
|
|
1db1442319 | ||
|
|
362dcb2a65 | ||
|
|
d1abcc00a2 | ||
|
|
0ce874f8d4 | ||
|
|
282514f755 | ||
|
|
d1e4650e19 | ||
|
|
370f87ea01 | ||
|
|
8b9a73a735 | ||
|
|
29c69b3d8f | ||
|
|
4b5a84dd81 | ||
|
|
b9874c8114 | ||
|
|
b1131795d8 | ||
|
|
5e990b3991 | ||
|
|
ff1951c715 | ||
|
|
121c0690b0 | ||
|
|
16b0853654 | ||
|
|
7d639cf880 | ||
|
|
22f3575064 | ||
|
|
d4069aeae5 | ||
|
|
54d5a788a5 | ||
|
|
f66220a245 | ||
|
|
2266060e84 | ||
|
|
90039d7f4d | ||
|
|
16eb66d7cf | ||
|
|
02bab00be6 | ||
|
|
55a56914d7 |
@@ -23,3 +23,9 @@ lib/EpdFont/scripts/output/
|
|||||||
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
||||||
.claude/*
|
.claude/*
|
||||||
!.claude/skills/
|
!.claude/skills/
|
||||||
|
/managed_components
|
||||||
|
/.dummy
|
||||||
|
dependencies.lock
|
||||||
|
sdkconfig.default
|
||||||
|
sdkconfig.defaults
|
||||||
|
CMakeLists.txt
|
||||||
|
|||||||
+4
-3
@@ -1,3 +1,4 @@
|
|||||||
[submodule "open-x4-sdk"]
|
[submodule "freeink-sdk"]
|
||||||
path = open-x4-sdk
|
path = freeink-sdk
|
||||||
url = https://github.com/crosspoint-reader/community-sdk.git
|
url = https://github.com/Free-Ink/freeink-sdk.git
|
||||||
|
branch = main
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
|
|||||||
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
||||||
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
||||||
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
|
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
|
||||||
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
|
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
|
||||||
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
|
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
|
||||||
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
|
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
|
||||||
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
|
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
|
||||||
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
|||||||
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
||||||
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
||||||
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
||||||
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
* freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
||||||
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
||||||
|
|
||||||
### Hardware Abstraction Layer (HAL)
|
### Hardware Abstraction Layer (HAL)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs.
|
||||||
|
|
||||||
## What can CrossPoint do?
|
## What can CrossPoint do?
|
||||||
|
|
||||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||||
@@ -67,10 +69,6 @@ USB port or browser before assuming the device is locked. Only reach for the unl
|
|||||||
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
|
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
|
||||||
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
|
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
|
||||||
> the firmware you flashed doesn't support OTA, **there is no way out**.
|
> the firmware you flashed doesn't support OTA, **there is no way out**.
|
||||||
>
|
|
||||||
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
|
|
||||||
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
|
|
||||||
> Papyrix (or any other unsupported firmware) on a locked device.**
|
|
||||||
|
|
||||||
## Install firmware
|
## Install firmware
|
||||||
|
|
||||||
@@ -249,7 +247,7 @@ One of the best things about open source is that anyone can take the code in a d
|
|||||||
|
|
||||||
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
|
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
|
||||||
|
|
||||||
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
|
- ~~[crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.~~ (Unmaintained)
|
||||||
|
|
||||||
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
||||||
|
|
||||||
|
|||||||
+64
-21
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [CrossPoint User Guide](#crosspoint-user-guide)
|
- [CrossPoint User Guide](#crosspoint-user-guide)
|
||||||
- [1. Hardware Overview](#1-hardware-overview)
|
- [1. Hardware Overview](#1-hardware-overview)
|
||||||
- [Button Layout](#button-layout)
|
- [Button Layout](#button-layout)
|
||||||
|
- [Taking a Screenshot](#taking-a-screenshot)
|
||||||
- [2. Power \& Startup](#2-power--startup)
|
- [2. Power \& Startup](#2-power--startup)
|
||||||
- [Power On / Off](#power-on--off)
|
- [Power On / Off](#power-on--off)
|
||||||
- [First Launch](#first-launch)
|
- [First Launch](#first-launch)
|
||||||
@@ -14,7 +15,11 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
||||||
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
||||||
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
||||||
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
||||||
|
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
|
||||||
|
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
|
||||||
|
- [Uploading Books](#uploading-books)
|
||||||
|
- [Removing a Book](#removing-a-book)
|
||||||
- [3.6 Settings](#36-settings)
|
- [3.6 Settings](#36-settings)
|
||||||
- [3.6.1 Display](#361-display)
|
- [3.6.1 Display](#361-display)
|
||||||
- [3.6.2 Reader](#362-reader)
|
- [3.6.2 Reader](#362-reader)
|
||||||
@@ -23,21 +28,25 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
||||||
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
||||||
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
||||||
|
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
|
||||||
|
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
|
||||||
- [3.7 Sleep Screen](#37-sleep-screen)
|
- [3.7 Sleep Screen](#37-sleep-screen)
|
||||||
|
- [Cover settings](#cover-settings)
|
||||||
|
- [Custom images](#custom-images)
|
||||||
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
|
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
|
||||||
- [4. Reading Mode](#4-reading-mode)
|
- [4. Reading Mode](#4-reading-mode)
|
||||||
- [Page Turning](#page-turning)
|
- [Page Turning](#page-turning)
|
||||||
- [Chapter Navigation](#chapter-navigation)
|
- [Chapter Navigation](#chapter-navigation)
|
||||||
- [Auto Page Turn](#auto-page-turn)
|
- [Auto Page Turn](#auto-page-turn)
|
||||||
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
|
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
|
||||||
- [Footnote Navigation](#footnote-navigation)
|
- [Footnote Navigation](#footnote-navigation)
|
||||||
- [System Navigation](#system-navigation)
|
- [System Navigation](#system-navigation)
|
||||||
- [Supported Languages](#supported-languages)
|
- [Supported Languages](#supported-languages)
|
||||||
- [5. Reader Menu](#5-reader-menu)
|
- [5. Reader Menu](#5-reader-menu)
|
||||||
- [5.1 Chapter Selection](#51-chapter-selection)
|
- [5.1 Chapter Selection](#51-chapter-selection)
|
||||||
- [5.2 Bookmarks](#52-bookmarks)
|
- [5.2 Bookmarks](#52-bookmarks)
|
||||||
- [6. Current Limitations & Roadmap](#6-current-limitations--roadmap)
|
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||||
- [7. Troubleshooting Issues & Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||||
|
|
||||||
## 1. Hardware Overview
|
## 1. Hardware Overview
|
||||||
|
|
||||||
@@ -122,19 +131,43 @@ A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined
|
|||||||
|
|
||||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||||
|
|
||||||
1. Install the plugin in Calibre:
|
#### Installing the Plugin in Calibre
|
||||||
|
|
||||||
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
If you don't already have the plugin installed:
|
||||||
|
|
||||||
- Download the zip file.
|
1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||||
|
2. Download the zip file.
|
||||||
|
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||||
|
4. Restart Calibre.
|
||||||
|
|
||||||
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
#### Configuring the CrossPoint Plugin in Calibre
|
||||||
|
1. In Calibre select Preferences.
|
||||||
|
2. In the Preferences dialog select Plugins.
|
||||||
|
3. In Plugins search for "crosspoint".
|
||||||
|
4. Click on "Customize plugin".
|
||||||
|
5. Update the value for "Host" to match the IP for your device.
|
||||||
|
6. Leave the other settings as they are.
|
||||||
|
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
|
||||||
|
8. Restart Calibre.
|
||||||
|
|
||||||
2. On the device: File Transfer -> Calibre Wireless, then join a network.
|
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
|
||||||
|
|
||||||
3. Make sure your computer is on the same Wi-Fi network.
|
#### Uploading Books
|
||||||
|
|
||||||
4. In Calibre, click "Send to device" to transfer books.
|
To upload a book using the CrossPoint plugin in Calibre:
|
||||||
|
|
||||||
|
1. On the device: File Transfer -> Calibre Wireless, then join a network.
|
||||||
|
2. Select one or more books.
|
||||||
|
3. Right-click on that selection.
|
||||||
|
4. Select "Send to Device" > "Send to main memory"
|
||||||
|
|
||||||
|
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
|
||||||
|
|
||||||
|
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
|
||||||
|
|
||||||
|
#### Removing a Book
|
||||||
|
|
||||||
|
Books cannot be removed from your device through Calibre. Use the web interface instead.
|
||||||
|
|
||||||
### 3.6 Settings
|
### 3.6 Settings
|
||||||
|
|
||||||
@@ -150,6 +183,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||||
- "None" - A blank screen
|
- "None" - A blank screen
|
||||||
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
|
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
|
||||||
|
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
|
||||||
|
|
||||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||||
|
|
||||||
@@ -162,6 +196,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||||
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
|
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
|
||||||
|
|
||||||
|
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
|
||||||
|
|
||||||
- **Status Bar**: Configure the status bar displayed while reading:
|
- **Status Bar**: Configure the status bar displayed while reading:
|
||||||
|
|
||||||
- "None" - No status bar
|
- "None" - No status bar
|
||||||
@@ -241,6 +277,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
|
|
||||||
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||||
- "Page Scroll" - Long-pressing scrolls a page up/down
|
- "Page Scroll" - Long-pressing scrolls a page up/down
|
||||||
|
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
|
||||||
|
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
|
||||||
|
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
|
||||||
|
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
|
||||||
|
|
||||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||||
|
|
||||||
@@ -523,11 +563,14 @@ On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the dev
|
|||||||
|
|
||||||
When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position.
|
When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position.
|
||||||
|
|
||||||
|
If the device goes to sleep or you close the book while viewing a footnote, the book reopens to your original reading position, not the footnote.
|
||||||
|
|
||||||
### System Navigation
|
### System Navigation
|
||||||
|
|
||||||
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||||
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
||||||
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
|
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
|
||||||
|
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Disabled" does nothing. A short press always opens the Reader Menu.
|
||||||
|
|
||||||
### Supported Languages
|
### Supported Languages
|
||||||
|
|
||||||
@@ -574,9 +617,9 @@ Accessible by selecting **Chapters** from the Reader Menu.
|
|||||||
|
|
||||||
Bookmarks can be created to quickly save and restore your place in a book.
|
Bookmarks can be created to quickly save and restore your place in a book.
|
||||||
|
|
||||||
To create a bookmark, hold **Confirm** for 1 second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds.
|
To create a bookmark, hold **Confirm** for about half a second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds.
|
||||||
|
|
||||||
To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for 1 second, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel.
|
To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for about 0.7 seconds, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel.
|
||||||
|
|
||||||
Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format.
|
Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format.
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
|||||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||||
| grep -v -E '^lib/uzlib/' \
|
| grep -v -E '^lib/uzlib/' \
|
||||||
|
| grep -v -E '^lib/miniz/third_party/' \
|
||||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||||
# Restore strict pipeline failure handling for the rest of the script.
|
# Restore strict pipeline failure handling for the rest of the script.
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Formats all C/C++ source and header files in the repository, excluding
|
Formats all C/C++ source and header files in the repository, excluding
|
||||||
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
|
generated, vendored, and build directories (freeink-sdk, builtinFonts,
|
||||||
hyphenation tries, uzlib, .pio, *.generated.h).
|
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||||
|
|
||||||
The clang-format binary path is resolved once and cached in
|
The clang-format binary path is resolved once and cached in
|
||||||
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
|
|||||||
$clangFormat = Resolve-ClangFormat
|
$clangFormat = Resolve-ClangFormat
|
||||||
|
|
||||||
$exclude = @(
|
$exclude = @(
|
||||||
'open-x4-sdk'
|
'freeink-sdk'
|
||||||
'lib\EpdFont\builtinFonts'
|
'lib\EpdFont\builtinFonts'
|
||||||
'lib\Epub\Epub\hyphenation\generated'
|
'lib\Epub\Epub\hyphenation\generated'
|
||||||
'lib\uzlib'
|
'lib\uzlib'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ At a high level, it is firmware that uses an activity-driven application archite
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk]
|
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
|
||||||
B --> C[lib/hal wrappers]
|
B --> C[lib/hal wrappers]
|
||||||
C --> D[src/main.cpp runtime loop]
|
C --> D[src/main.cpp runtime loop]
|
||||||
D --> E[Activities layer]
|
D --> E[Activities layer]
|
||||||
@@ -195,10 +195,10 @@ When editing related source assets, regenerate via normal build steps/scripts.
|
|||||||
- `src/`: app orchestration, settings/state, and activity implementations
|
- `src/`: app orchestration, settings/state, and activity implementations
|
||||||
- `src/network/`: web server and OTA/update networking
|
- `src/network/`: web server and OTA/update networking
|
||||||
- `src/components/`: theming and shared UI components
|
- `src/components/`: theming and shared UI components
|
||||||
- `lib/hal/`: hardware abstraction wrappers around open-x4-sdk
|
- `lib/hal/`: hardware abstraction wrappers around freeink-sdk
|
||||||
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
||||||
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
||||||
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
|
- `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
|
||||||
- `docs/`: user and technical documentation
|
- `docs/`: user and technical documentation
|
||||||
|
|
||||||
## Embedded constraints that shape design
|
## Embedded constraints that shape design
|
||||||
|
|||||||
+28
-10
@@ -90,13 +90,19 @@ if (parsedSize != fileSize) {
|
|||||||
|
|
||||||
## `section.bin`
|
## `section.bin`
|
||||||
|
|
||||||
### Version 25
|
### Version 30
|
||||||
|
|
||||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
||||||
also the cache-busting key: if any layout-affecting setting differs from the
|
also the cache-busting key: if any layout-affecting setting differs from the
|
||||||
current reader settings, the section is discarded and rebuilt.
|
current reader settings, the section is discarded and rebuilt.
|
||||||
|
|
||||||
Version 25 includes:
|
Version 30 is binary-identical to version 29. The version was bumped because
|
||||||
|
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
|
||||||
|
measures the shaped visual text), so word positions cached by v29 no longer
|
||||||
|
match what `drawText` renders.
|
||||||
|
|
||||||
|
Version 28 introduced serialized word style bits for underline, strikethrough,
|
||||||
|
superscript, and subscript. The format also includes:
|
||||||
|
|
||||||
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
||||||
image rendering mode, and Focus Reading
|
image rendering mode, and Focus Reading
|
||||||
@@ -105,6 +111,12 @@ Version 25 includes:
|
|||||||
- paragraph and list-item LUTs used by KOReader sync page refinement
|
- paragraph and list-item LUTs used by KOReader sync page refinement
|
||||||
- optional per-word Focus Reading split metadata
|
- optional per-word Focus Reading split metadata
|
||||||
- per-page footnote entries
|
- per-page footnote entries
|
||||||
|
- serialized word style bits for underline, strikethrough, superscript, and
|
||||||
|
subscript
|
||||||
|
- flat TextBlock word storage (v29): per-word arrays plus one shared
|
||||||
|
NUL-terminated text blob, replacing v28's length-prefixed word strings. The
|
||||||
|
on-disk order mirrors the in-RAM arena so the firmware reads a whole block
|
||||||
|
payload with a single allocation and a single SD read
|
||||||
|
|
||||||
ImHex pattern:
|
ImHex pattern:
|
||||||
|
|
||||||
@@ -113,7 +125,7 @@ import std.mem;
|
|||||||
import std.string;
|
import std.string;
|
||||||
import std.core;
|
import std.core;
|
||||||
|
|
||||||
#define EXPECTED_VERSION 25
|
#define EXPECTED_VERSION 30
|
||||||
#define MAX_STRING_LENGTH 65535
|
#define MAX_STRING_LENGTH 65535
|
||||||
#define FOOTNOTE_NUMBER_LEN 32
|
#define FOOTNOTE_NUMBER_LEN 32
|
||||||
#define FOOTNOTE_HREF_LEN 96
|
#define FOOTNOTE_HREF_LEN 96
|
||||||
@@ -174,14 +186,20 @@ struct BlockStyle {
|
|||||||
|
|
||||||
struct TextBlock {
|
struct TextBlock {
|
||||||
u16 wordCount;
|
u16 wordCount;
|
||||||
String words[wordCount];
|
|
||||||
s16 wordXPos[wordCount];
|
|
||||||
WordStyle wordStyle[wordCount];
|
|
||||||
|
|
||||||
u8 hasFocus;
|
u8 hasFocus;
|
||||||
if (hasFocus != 0) {
|
u16 textBytes [[comment("Total size of text[], including one NUL per word")]];
|
||||||
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
|
||||||
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
if (wordCount > 0) {
|
||||||
|
u16 textOff[wordCount] [[comment("Byte offset of word i's text within text[]")]];
|
||||||
|
s16 wordXPos[wordCount];
|
||||||
|
if (hasFocus != 0) {
|
||||||
|
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
||||||
|
}
|
||||||
|
WordStyle wordStyle[wordCount];
|
||||||
|
if (hasFocus != 0) {
|
||||||
|
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
||||||
|
}
|
||||||
|
char text[textBytes] [[comment("All words back to back, each NUL-terminated")]];
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
+8
-3
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
|
|||||||
## Join Network Mode
|
## Join Network Mode
|
||||||
|
|
||||||
1. Select **Join Network**.
|
1. Select **Join Network**.
|
||||||
2. Pick a 2.4 GHz Wi-Fi network from the scan results.
|
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
|
||||||
3. Enter the password if prompted.
|
connected network, then other visible saved networks in signal-strength
|
||||||
4. Save credentials if you want the reader to reconnect automatically next time.
|
order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
|
||||||
|
the network list.
|
||||||
|
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
|
||||||
|
results.
|
||||||
|
4. Enter the password if prompted.
|
||||||
|
5. Save credentials if you want the reader to reconnect automatically next time.
|
||||||
|
|
||||||
After connection, the reader shows:
|
After connection, the reader shows:
|
||||||
|
|
||||||
|
|||||||
Submodule
+1
Submodule freeink-sdk added at 421d75d011
@@ -44,16 +44,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
|
const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
|
||||||
|
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
|
||||||
|
|
||||||
if (!isCombining && prevCp != 0) {
|
if (!isCombining && prevCp != 0) {
|
||||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int glyphBaseX =
|
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||||
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
|
glyph->left, glyph->width)
|
||||||
: lastBaseX;
|
: lastBaseX;
|
||||||
const int glyphBaseY = startY - raiseBy;
|
const int glyphBaseY = startY - raiseBy;
|
||||||
|
|
||||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||||
@@ -101,6 +102,9 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
|
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||||
|
if (utf8IsCjkBreakable(leftCp) || utf8IsCjkBreakable(rightCp)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (!data->kernMatrix) {
|
if (!data->kernMatrix) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-11
@@ -36,24 +36,71 @@ namespace combiningMark {
|
|||||||
|
|
||||||
constexpr int MIN_GAP_PX = 1;
|
constexpr int MIN_GAP_PX = 1;
|
||||||
|
|
||||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
/// Placement of a mark relative to its base glyph. The default heuristic —
|
||||||
/// is visually centered over the base glyph's bitmap.
|
/// centered over the base, raised clear of its top — suits Latin diacritics
|
||||||
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
|
||||||
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
|
/// on position: dagesh sits inside the letter body, the shin/sin dots
|
||||||
|
/// distinguish the letter by sitting over its right/left arm, and holam hangs
|
||||||
|
/// over the left corner. "Native" anchors keep the glyph's font-designed
|
||||||
|
/// height (which may overlap the base) instead of raising it.
|
||||||
|
enum class Anchor : uint8_t {
|
||||||
|
CenterRaised, ///< centered over the base, lifted above its top (default)
|
||||||
|
CenterNative, ///< centered over the base at font-native height
|
||||||
|
RightNative, ///< right edges aligned, font-native height
|
||||||
|
LeftNative, ///< left edges aligned, font-native height
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Anchor anchorFor(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
|
||||||
|
case 0x05BA: // holam haser for vav: straight above the vav stem
|
||||||
|
return Anchor::CenterNative;
|
||||||
|
case 0x05C1: // shin dot: over the letter's right arm
|
||||||
|
return Anchor::RightNative;
|
||||||
|
case 0x05B9: // holam: above the letter's left corner
|
||||||
|
case 0x05C2: // sin dot: over the letter's left arm
|
||||||
|
return Anchor::LeftNative;
|
||||||
|
default:
|
||||||
|
return Anchor::CenterRaised;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rotated-90CW variant of centerOver. In the rotated coordinate system
|
/// Horizontal offset from the base bitmap's left edge to the mark bitmap's
|
||||||
|
/// left edge for a given anchor.
|
||||||
|
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
|
||||||
|
switch (anchor) {
|
||||||
|
case Anchor::LeftNative:
|
||||||
|
return 0;
|
||||||
|
case Anchor::RightNative:
|
||||||
|
return baseWidth - markWidth;
|
||||||
|
default:
|
||||||
|
return baseWidth / 2 - markWidth / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||||
|
/// lands at its anchor position over the base glyph's bitmap.
|
||||||
|
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
|
||||||
|
const int markLeft, const int markWidth) {
|
||||||
|
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
|
||||||
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
||||||
/// every left/width term inverts sign.
|
/// every left/width term inverts sign.
|
||||||
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
|
||||||
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft;
|
const int baseWidth, const int markLeft, const int markWidth) {
|
||||||
|
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For combining marks that sit entirely above the baseline, compute how many
|
/// For combining marks that sit entirely above the baseline, compute how many
|
||||||
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
||||||
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
||||||
/// below the baseline (e.g. cedilla, dot-below, ogonek).
|
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
|
||||||
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
/// keep the font-native height (dagesh must stay inside the letter, the
|
||||||
|
/// shin/sin dots touch its arms).
|
||||||
|
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
|
||||||
|
if (anchor != Anchor::CenterRaised) return 0;
|
||||||
if (markTop - markHeight <= 0) return 0;
|
if (markTop - markHeight <= 0) return 0;
|
||||||
const int gap = markTop - markHeight - baseTop;
|
const int gap = markTop - markHeight - baseTop;
|
||||||
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
||||||
@@ -61,6 +108,20 @@ constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
|||||||
|
|
||||||
} // namespace combiningMark
|
} // namespace combiningMark
|
||||||
|
|
||||||
|
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
|
||||||
|
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
|
||||||
|
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
|
||||||
|
/// identical across both toolchains.
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
|
||||||
|
#define EPD_PACKED_END __pragma(pack(pop))
|
||||||
|
#define EPD_PACKED_ATTR
|
||||||
|
#else
|
||||||
|
#define EPD_PACKED_BEGIN
|
||||||
|
#define EPD_PACKED_END
|
||||||
|
#define EPD_PACKED_ATTR __attribute__((packed))
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||||
@@ -95,17 +156,21 @@ typedef struct {
|
|||||||
|
|
||||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint16_t codepoint; ///< Unicode codepoint
|
uint16_t codepoint; ///< Unicode codepoint
|
||||||
uint8_t classId; ///< 1-based kerning class ID
|
uint8_t classId; ///< 1-based kerning class ID
|
||||||
} __attribute__((packed)) EpdKernClassEntry;
|
} EPD_PACKED_ATTR EpdKernClassEntry;
|
||||||
|
EPD_PACKED_END
|
||||||
|
|
||||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||||
} __attribute__((packed)) EpdLigaturePair;
|
} EPD_PACKED_ATTR EpdLigaturePair;
|
||||||
|
EPD_PACKED_END
|
||||||
|
|
||||||
/// Data stored for FONT AS A WHOLE
|
/// Data stored for FONT AS A WHOLE
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "EpdFontFamily.h"
|
#include "EpdFontFamily.h"
|
||||||
|
|
||||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||||
// Extract font style bits (ignore UNDERLINE bit for font selection)
|
// Extract font style bits; render-time overlay bits do not affect font selection.
|
||||||
const bool hasBold = (style & BOLD) != 0;
|
const bool hasBold = (style & BOLD) != 0;
|
||||||
const bool hasItalic = (style & ITALIC) != 0;
|
const bool hasItalic = (style & ITALIC) != 0;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class EpdFontFamily {
|
|||||||
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
|
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
|
||||||
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
|
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
|
||||||
};
|
};
|
||||||
|
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
|
||||||
|
|
||||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||||
const EpdFont* boldItalic = nullptr)
|
const EpdFont* boldItalic = nullptr)
|
||||||
@@ -27,6 +28,9 @@ class EpdFontFamily {
|
|||||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
||||||
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
||||||
|
static constexpr bool hasTextDecoration(const Style style) {
|
||||||
|
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const EpdFont* regular;
|
const EpdFont* regular;
|
||||||
|
|||||||
@@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FontDecompressor::freeHotGroup() {
|
void FontDecompressor::freeHotGroup() {
|
||||||
hotGroup.clear();
|
free(hotGroup);
|
||||||
hotGroup.shrink_to_fit();
|
hotGroup = nullptr;
|
||||||
|
hotGroupCapacity = 0;
|
||||||
hotGroupFont = nullptr;
|
hotGroupFont = nullptr;
|
||||||
hotGroupIndex = UINT16_MAX;
|
hotGroupIndex = UINT16_MAX;
|
||||||
hotGlyphBuf.clear();
|
free(hotGlyphBuf);
|
||||||
hotGlyphBuf.shrink_to_fit();
|
hotGlyphBuf = nullptr;
|
||||||
|
hotGlyphBufCapacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) {
|
||||||
|
if (capacity >= needed) return true;
|
||||||
|
// Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the
|
||||||
|
// old contents are dead -- freeing first gives the allocator its best shot on a tight heap.
|
||||||
|
free(buf);
|
||||||
|
buf = static_cast<uint8_t*>(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup()
|
||||||
|
capacity = buf ? needed : 0;
|
||||||
|
return buf != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||||
@@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if hot group already has this group decompressed — if not, decompress it
|
// Check if hot group already has this group decompressed — if not, decompress it
|
||||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||||
stats.cacheMisses++;
|
stats.cacheMisses++;
|
||||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||||
|
|
||||||
hotGroup.resize(group.uncompressedSize);
|
// ensureCapacity may free the buffer, so the cached-group identity dies with it either way.
|
||||||
if (hotGroup.empty()) {
|
hotGroupFont = nullptr;
|
||||||
|
hotGroupIndex = UINT16_MAX;
|
||||||
|
if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) {
|
||||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||||
hotGroupFont = nullptr;
|
|
||||||
hotGroupIndex = UINT16_MAX;
|
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) {
|
||||||
hotGroup.clear();
|
|
||||||
hotGroup.shrink_to_fit();
|
|
||||||
hotGroupFont = nullptr;
|
|
||||||
hotGroupIndex = UINT16_MAX;
|
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) {
|
||||||
hotGlyphBuf.resize(glyph->dataLength);
|
LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength);
|
||||||
}
|
|
||||||
if (hotGlyphBuf.empty()) {
|
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height);
|
||||||
stats.getBitmapTimeUs += micros() - tStart;
|
stats.getBitmapTimeUs += micros() - tStart;
|
||||||
return hotGlyphBuf.data();
|
return hotGlyphBuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||||
@@ -363,6 +369,11 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
|||||||
}
|
}
|
||||||
stats.pageBufferBytes += totalBytes;
|
stats.pageBufferBytes += totalBytes;
|
||||||
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
|
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
|
||||||
|
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
|
||||||
|
// Landmark for the heap block map: page slots are the largest flash-font
|
||||||
|
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
|
||||||
|
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
|
||||||
|
(unsigned)glyphCount);
|
||||||
|
|
||||||
slot.fontData = fontData;
|
slot.fontData = fontData;
|
||||||
slot.glyphCount = glyphCount;
|
slot.glyphCount = glyphCount;
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
#include <InflateReader.h>
|
#include <InflateReader.h>
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
|
|
||||||
class FontDecompressor {
|
class FontDecompressor {
|
||||||
@@ -67,13 +65,22 @@ class FontDecompressor {
|
|||||||
|
|
||||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
||||||
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||||
|
// Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path,
|
||||||
|
// and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of
|
||||||
|
// failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free).
|
||||||
|
// ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully.
|
||||||
const EpdFontData* hotGroupFont = nullptr;
|
const EpdFontData* hotGroupFont = nullptr;
|
||||||
uint16_t hotGroupIndex = UINT16_MAX;
|
uint16_t hotGroupIndex = UINT16_MAX;
|
||||||
std::vector<uint8_t> hotGroup;
|
uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor
|
||||||
|
uint32_t hotGroupCapacity = 0;
|
||||||
|
|
||||||
// Scratch buffer for compacting a single glyph from the hot group.
|
// Scratch buffer for compacting a single glyph from the hot group.
|
||||||
// Valid until the next getBitmap() call.
|
// Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup.
|
||||||
std::vector<uint8_t> hotGlyphBuf;
|
uint8_t* hotGlyphBuf = nullptr;
|
||||||
|
uint32_t hotGlyphBufCapacity = 0;
|
||||||
|
|
||||||
|
// Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed.
|
||||||
|
static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed);
|
||||||
|
|
||||||
void freePageBuffer();
|
void freePageBuffer();
|
||||||
void freeHotGroup();
|
void freeHotGroup();
|
||||||
|
|||||||
+162
-63
@@ -68,6 +68,22 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c
|
|||||||
const char* asCStr(const std::string& s) { return s.c_str(); }
|
const char* asCStr(const std::string& s) { return s.c_str(); }
|
||||||
const char* asCStr(const char* s) { return s; }
|
const char* asCStr(const char* s) { return s; }
|
||||||
|
|
||||||
|
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
|
||||||
|
// current capacity. Freeing + reallocating slightly different sizes every page
|
||||||
|
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
|
||||||
|
// next page's need), eroding the largest contiguous block all session. With
|
||||||
|
// reuse, capacities converge on the book's max page after a few turns and page
|
||||||
|
// turns stop touching the allocator. Only three small instantiations exist
|
||||||
|
// (interval/glyph/byte arrays), so template bloat is negligible.
|
||||||
|
template <typename T, typename CapT>
|
||||||
|
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
|
||||||
|
if (buf && capacity >= needed) return true;
|
||||||
|
delete[] buf;
|
||||||
|
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
|
||||||
|
capacity = buf ? static_cast<CapT>(needed) : 0;
|
||||||
|
return buf != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SdCardFont::~SdCardFont() { freeAll(); }
|
SdCardFont::~SdCardFont() { freeAll(); }
|
||||||
@@ -83,6 +99,9 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
s.miniBitmap = nullptr;
|
s.miniBitmap = nullptr;
|
||||||
s.miniIntervalCount = 0;
|
s.miniIntervalCount = 0;
|
||||||
s.miniGlyphCount = 0;
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniIntervalCapacity = 0;
|
||||||
|
s.miniGlyphCapacity = 0;
|
||||||
|
s.miniBitmapCapacity = 0;
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
memset(&s.miniData, 0, sizeof(s.miniData));
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
s.epdFont.data = &s.stubData;
|
s.epdFont.data = &s.stubData;
|
||||||
@@ -109,12 +128,18 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
|||||||
s.miniKernRightEntryCount = 0;
|
s.miniKernRightEntryCount = 0;
|
||||||
s.miniKernLeftClassCount = 0;
|
s.miniKernLeftClassCount = 0;
|
||||||
s.miniKernRightClassCount = 0;
|
s.miniKernRightClassCount = 0;
|
||||||
|
s.miniKernLeftCapacity = 0;
|
||||||
|
s.miniKernRightCapacity = 0;
|
||||||
|
s.miniKernMatrixCapacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdCardFont::freeStyleAll(PerStyle& s) {
|
void SdCardFont::freeStyleAll(PerStyle& s) {
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
delete[] s.fullIntervals;
|
delete[] s.fullIntervals;
|
||||||
s.fullIntervals = nullptr;
|
s.fullIntervals = nullptr;
|
||||||
|
delete[] s.bmpIntervals;
|
||||||
|
s.bmpIntervals = nullptr;
|
||||||
|
s.intervalsAreBmp16 = false;
|
||||||
freeStyleKernLigatureData(s);
|
freeStyleKernLigatureData(s);
|
||||||
s.present = false;
|
s.present = false;
|
||||||
}
|
}
|
||||||
@@ -308,13 +333,13 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui
|
|||||||
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
|
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: allocate the three mini buffers. The matrix is <1KB in practice
|
// Step 4: size the three mini buffers (reused across pages when they fit; the
|
||||||
// (<30 × <30 × 1 byte) so fragmentation is a non-issue.
|
// per-page sizes vary by a few entries, which as free+realloc churn was punching
|
||||||
|
// non-coalescing holes in the heap every page turn).
|
||||||
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
||||||
s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount];
|
if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) ||
|
||||||
s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount];
|
!ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) ||
|
||||||
s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes];
|
!ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) {
|
||||||
if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
|
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
|
||||||
matrixBytes);
|
matrixBytes);
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
@@ -516,59 +541,94 @@ bool SdCardFont::load(const char* path) {
|
|||||||
styleCount_ = styleCount;
|
styleCount_ = styleCount;
|
||||||
contentHash_ = hash;
|
contentHash_ = hash;
|
||||||
|
|
||||||
// Load full intervals into RAM for each present style
|
// Load full intervals into RAM for each present style. BMP-only fonts with
|
||||||
|
// fewer than 65536 glyphs use a compact 6-byte interval table instead of the
|
||||||
|
// on-disk 12-byte table; large sparse CJK subsets otherwise keep tens of KB
|
||||||
|
// of always-resident heap just for lookup metadata.
|
||||||
for (uint8_t i = 0; i < MAX_STYLES; i++) {
|
for (uint8_t i = 0; i < MAX_STYLES; i++) {
|
||||||
auto& s = styles_[i];
|
auto& s = styles_[i];
|
||||||
if (!s.present) continue;
|
if (!s.present) continue;
|
||||||
|
|
||||||
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
|
|
||||||
if (!s.fullIntervals) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
|
|
||||||
freeAll();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.seekSet(s.intervalsFileOffset)) {
|
if (!file.seekSet(s.intervalsFileOffset)) {
|
||||||
LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i);
|
LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i);
|
||||||
freeAll();
|
freeAll();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
|
|
||||||
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
|
|
||||||
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
|
|
||||||
freeAll();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate interval contents before any later code (findGlobalGlyphIndex,
|
// Validate interval contents before any later code (findGlobalGlyphIndex,
|
||||||
// glyph reads) trusts them. A malformed file could otherwise drive
|
// glyph reads) trusts them. A malformed file could otherwise drive
|
||||||
// out-of-range glyph indices into bogus on-disk reads.
|
// out-of-range glyph indices into bogus on-disk reads.
|
||||||
{
|
bool canUseBmp16 = s.header.glyphCount <= UINT16_MAX;
|
||||||
uint32_t expectedOffset = 0;
|
uint32_t expectedOffset = 0;
|
||||||
uint32_t prevLast = 0;
|
uint32_t prevLast = 0;
|
||||||
|
EpdUnicodeInterval iv{};
|
||||||
|
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
|
||||||
|
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
|
||||||
|
LOG_ERR("SDCF", "Failed to read interval %u for style %u", j, i);
|
||||||
|
freeAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (iv.first > iv.last) {
|
||||||
|
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
|
||||||
|
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
|
||||||
|
file.close();
|
||||||
|
freeAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint32_t span = iv.last - iv.first + 1;
|
||||||
|
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
|
||||||
|
const bool spanTooBig = (span > s.header.glyphCount);
|
||||||
|
const bool offsetMismatch = (iv.offset != expectedOffset);
|
||||||
|
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
|
||||||
|
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
|
||||||
|
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
|
||||||
|
overlapsPrev, span, offsetMismatch, offsetOverruns);
|
||||||
|
file.close();
|
||||||
|
freeAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (iv.first > UINT16_MAX || iv.last > UINT16_MAX || iv.offset > UINT16_MAX) {
|
||||||
|
canUseBmp16 = false;
|
||||||
|
}
|
||||||
|
expectedOffset += span;
|
||||||
|
prevLast = iv.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.seekSet(s.intervalsFileOffset)) {
|
||||||
|
LOG_ERR("SDCF", "Failed to seek back to intervals for style %u", i);
|
||||||
|
freeAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseBmp16) {
|
||||||
|
s.bmpIntervals = new (std::nothrow) PerStyle::BmpInterval16[s.header.intervalCount];
|
||||||
|
if (!s.bmpIntervals) {
|
||||||
|
LOG_ERR("SDCF", "Failed to allocate compact intervals for style %u", i);
|
||||||
|
freeAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
|
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
|
||||||
const auto& iv = s.fullIntervals[j];
|
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
|
||||||
if (iv.first > iv.last) {
|
LOG_ERR("SDCF", "Failed to read compact interval %u for style %u", j, i);
|
||||||
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
|
|
||||||
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
|
|
||||||
file.close();
|
|
||||||
freeAll();
|
freeAll();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const uint32_t span = iv.last - iv.first + 1;
|
s.bmpIntervals[j] = {static_cast<uint16_t>(iv.first), static_cast<uint16_t>(iv.last),
|
||||||
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
|
static_cast<uint16_t>(iv.offset)};
|
||||||
const bool spanTooBig = (span > s.header.glyphCount);
|
}
|
||||||
const bool offsetMismatch = (iv.offset != expectedOffset);
|
s.intervalsAreBmp16 = true;
|
||||||
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
|
} else {
|
||||||
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
|
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
|
||||||
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
|
if (!s.fullIntervals) {
|
||||||
overlapsPrev, span, offsetMismatch, offsetOverruns);
|
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
|
||||||
file.close();
|
freeAll();
|
||||||
freeAll();
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
|
||||||
expectedOffset += span;
|
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
|
||||||
prevLast = iv.last;
|
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
|
||||||
|
freeAll();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,13 +663,15 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)
|
|||||||
int right = static_cast<int>(s.header.intervalCount) - 1;
|
int right = static_cast<int>(s.header.intervalCount) - 1;
|
||||||
while (left <= right) {
|
while (left <= right) {
|
||||||
int mid = left + (right - left) / 2;
|
int mid = left + (right - left) / 2;
|
||||||
const auto& interval = s.fullIntervals[mid];
|
const uint32_t first = s.intervalsAreBmp16 ? s.bmpIntervals[mid].first : s.fullIntervals[mid].first;
|
||||||
if (codepoint < interval.first) {
|
const uint32_t last = s.intervalsAreBmp16 ? s.bmpIntervals[mid].last : s.fullIntervals[mid].last;
|
||||||
|
if (codepoint < first) {
|
||||||
right = mid - 1;
|
right = mid - 1;
|
||||||
} else if (codepoint > interval.last) {
|
} else if (codepoint > last) {
|
||||||
left = mid + 1;
|
left = mid + 1;
|
||||||
} else {
|
} else {
|
||||||
return static_cast<int32_t>(interval.offset + (codepoint - interval.first));
|
const uint32_t offset = s.intervalsAreBmp16 ? s.bmpIntervals[mid].offset : s.fullIntervals[mid].offset;
|
||||||
|
return static_cast<int32_t>(offset + (codepoint - first));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@@ -753,12 +815,19 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
return missed;
|
return missed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build mini intervals from sorted codepoints
|
// Build mini intervals from sorted codepoints. Reset counts and fall back to the
|
||||||
freeStyleMiniData(s);
|
// stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits
|
||||||
|
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
|
||||||
|
s.miniIntervalCount = 0;
|
||||||
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniKernLeftEntryCount = 0;
|
||||||
|
s.miniKernRightEntryCount = 0;
|
||||||
|
s.miniKernLeftClassCount = 0;
|
||||||
|
s.miniKernRightClassCount = 0;
|
||||||
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
|
s.epdFont.data = &s.stubData;
|
||||||
|
|
||||||
uint32_t intervalCapacity = validCount;
|
if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) {
|
||||||
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
|
|
||||||
if (!s.miniIntervals) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
@@ -776,15 +845,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate mini glyph array
|
// Mini glyph array (reused across pages when it fits)
|
||||||
s.miniGlyphCount = validCount;
|
if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) {
|
||||||
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
|
|
||||||
if (!s.miniGlyphs) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
|
s.miniGlyphCount = validCount;
|
||||||
|
|
||||||
// Build sorted read order for sequential I/O
|
// Build sorted read order for sequential I/O
|
||||||
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
||||||
@@ -851,8 +919,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1];
|
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||||||
if (!s.miniBitmap) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||||
delete[] readOrder;
|
delete[] readOrder;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
@@ -1155,7 +1222,8 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename Iter>
|
template <typename Iter>
|
||||||
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||||
|
const char* extraText) {
|
||||||
if (!loaded_) return -1;
|
if (!loaded_) return -1;
|
||||||
styleMask = resolveStyleMask(styleMask);
|
styleMask = resolveStyleMask(styleMask);
|
||||||
if (styleMask == 0) return 0;
|
if (styleMask == 0) return 0;
|
||||||
@@ -1175,6 +1243,9 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
|||||||
for (auto it = begin; it != end && !hitCap; ++it) {
|
for (auto it = begin; it != end && !hitCap; ++it) {
|
||||||
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
}
|
}
|
||||||
|
if (extraText && !hitCap) {
|
||||||
|
hitCap = collectUniqueCodepoints(extraText, codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
|
}
|
||||||
|
|
||||||
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
||||||
codepoints[cpCount++] = ' ';
|
codepoints[cpCount++] = ' ';
|
||||||
@@ -1192,12 +1263,13 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
|||||||
return totalMissed;
|
return totalMissed;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) {
|
||||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
|
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
|
||||||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask);
|
const char* extraText) {
|
||||||
|
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Stats ---
|
// --- Stats ---
|
||||||
@@ -1257,7 +1329,7 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
|||||||
|
|
||||||
if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr;
|
if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr;
|
||||||
const auto& s = self->styles_[styleIdx];
|
const auto& s = self->styles_[styleIdx];
|
||||||
if (!s.fullIntervals) return nullptr;
|
if (!s.fullIntervals && !s.bmpIntervals) return nullptr;
|
||||||
|
|
||||||
// Check overflow cache first (matching both codepoint and style)
|
// Check overflow cache first (matching both codepoint and style)
|
||||||
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
||||||
@@ -1334,6 +1406,33 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
|||||||
return &self->overflow_[slot].glyph;
|
return &self->overflow_[slot].glyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t SdCardFont::reportMemory() const {
|
||||||
|
size_t total = 0;
|
||||||
|
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
|
||||||
|
const auto& s = styles_[si];
|
||||||
|
if (!s.present) continue;
|
||||||
|
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
|
||||||
|
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
|
||||||
|
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
|
||||||
|
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
|
||||||
|
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
|
||||||
|
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
|
||||||
|
// kept-if-fits mini arenas: capacity (not count) is what stays resident
|
||||||
|
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
|
||||||
|
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
|
||||||
|
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
|
||||||
|
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
|
||||||
|
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
|
||||||
|
total += fixed + mini + adv;
|
||||||
|
}
|
||||||
|
size_t overflowBytes = 0;
|
||||||
|
for (uint32_t i = 0; i < overflowCount_; ++i) {
|
||||||
|
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
|
||||||
|
}
|
||||||
|
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
|
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
|
||||||
for (uint32_t i = 0; i < overflowCount_; i++) {
|
for (uint32_t i = 0; i < overflowCount_; i++) {
|
||||||
if (&overflow_[i].glyph == glyph) return true;
|
if (&overflow_[i].glyph == glyph) return true;
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ class SdCardFont {
|
|||||||
// Build a compact advance-only table for layout measurement.
|
// Build a compact advance-only table for layout measurement.
|
||||||
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||||
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
||||||
|
// extraText: optional additional codepoints to warm in the same SD pass
|
||||||
|
// (e.g. shaped Arabic presentation forms the measurement path will look up).
|
||||||
// Returns number of codepoints not found in font coverage.
|
// Returns number of codepoints not found in font coverage.
|
||||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
|
||||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
|
||||||
|
const char* extraText = nullptr);
|
||||||
|
|
||||||
// Look up advanceX for a codepoint from the advance table.
|
// Look up advanceX for a codepoint from the advance table.
|
||||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||||
@@ -58,9 +61,9 @@ class SdCardFont {
|
|||||||
// Returns true if advance table is populated for at least one style.
|
// Returns true if advance table is populated for at least one style.
|
||||||
bool hasAdvanceTable() const;
|
bool hasAdvanceTable() const;
|
||||||
|
|
||||||
// Free mini data for all styles, restore stub EpdFontData.
|
// Free mini data for all styles and restore stub EpdFontData.
|
||||||
// Also clears the temporary advance table (built per layout pass) but
|
// Preserves the persistent advance cache so repeated layout passes can reuse
|
||||||
// preserves the persistent advance cache (reused across passes).
|
// previously fetched metrics.
|
||||||
void clearCache();
|
void clearCache();
|
||||||
|
|
||||||
// Drop the persistent advance cache. Call when unloading the SD font or
|
// Drop the persistent advance cache. Call when unloading the SD font or
|
||||||
@@ -101,6 +104,11 @@ class SdCardFont {
|
|||||||
uint32_t uniqueGlyphs = 0;
|
uint32_t uniqueGlyphs = 0;
|
||||||
uint32_t bitmapBytes = 0;
|
uint32_t bitmapBytes = 0;
|
||||||
};
|
};
|
||||||
|
// MEMFIX-PORT: SD font resident-bytes audit; portable
|
||||||
|
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
|
||||||
|
// advance tables + overflow bitmaps) and return the total in bytes. Pure
|
||||||
|
// accounting — no allocation, no state change.
|
||||||
|
size_t reportMemory() const;
|
||||||
void logStats(const char* label = "SDCF");
|
void logStats(const char* label = "SDCF");
|
||||||
void resetStats();
|
void resetStats();
|
||||||
const Stats& getStats() const { return stats_; }
|
const Stats& getStats() const { return stats_; }
|
||||||
@@ -140,6 +148,16 @@ class SdCardFont {
|
|||||||
|
|
||||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
|
struct BmpInterval16 {
|
||||||
|
uint16_t first;
|
||||||
|
uint16_t last;
|
||||||
|
uint16_t offset;
|
||||||
|
} EPD_PACKED_ATTR;
|
||||||
|
EPD_PACKED_END
|
||||||
|
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||||
|
BmpInterval16* bmpIntervals = nullptr;
|
||||||
|
bool intervalsAreBmp16 = false;
|
||||||
|
|
||||||
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
|
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
|
||||||
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
|
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
|
||||||
@@ -155,13 +173,22 @@ class SdCardFont {
|
|||||||
// Stub EpdFontData returned when not prewarmed
|
// Stub EpdFontData returned when not prewarmed
|
||||||
EpdFontData stubData{};
|
EpdFontData stubData{};
|
||||||
|
|
||||||
// Mini EpdFontData built during prewarm
|
// Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages
|
||||||
|
// (capacities below track allocated sizes): freeing and reallocating slightly
|
||||||
|
// different sizes on every page turn was a primary heap fragmenter — each page's
|
||||||
|
// freed hole rarely fit the next page's need, so maxAlloc eroded all session.
|
||||||
|
// After a few pages the capacities converge on the book's max and page turns
|
||||||
|
// stop allocating entirely. freeStyleMiniData() still releases everything (and
|
||||||
|
// zeroes capacities) for style eviction / font unload.
|
||||||
EpdFontData miniData{};
|
EpdFontData miniData{};
|
||||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||||
EpdGlyph* miniGlyphs = nullptr;
|
EpdGlyph* miniGlyphs = nullptr;
|
||||||
uint8_t* miniBitmap = nullptr;
|
uint8_t* miniBitmap = nullptr;
|
||||||
uint32_t miniIntervalCount = 0;
|
uint32_t miniIntervalCount = 0;
|
||||||
uint32_t miniGlyphCount = 0;
|
uint32_t miniGlyphCount = 0;
|
||||||
|
uint32_t miniIntervalCapacity = 0;
|
||||||
|
uint32_t miniGlyphCapacity = 0;
|
||||||
|
uint32_t miniBitmapCapacity = 0;
|
||||||
|
|
||||||
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
||||||
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
||||||
@@ -176,6 +203,10 @@ class SdCardFont {
|
|||||||
uint8_t miniKernLeftClassCount = 0;
|
uint8_t miniKernLeftClassCount = 0;
|
||||||
uint8_t miniKernRightClassCount = 0;
|
uint8_t miniKernRightClassCount = 0;
|
||||||
int8_t* miniKernMatrix = nullptr;
|
int8_t* miniKernMatrix = nullptr;
|
||||||
|
// Kept-if-fits capacities, same rationale as the mini glyph buffers above.
|
||||||
|
uint16_t miniKernLeftCapacity = 0;
|
||||||
|
uint16_t miniKernRightCapacity = 0;
|
||||||
|
uint32_t miniKernMatrixCapacity = 0;
|
||||||
|
|
||||||
// The EpdFont whose data pointer we manage
|
// The EpdFont whose data pointer we manage
|
||||||
EpdFont epdFont{&stubData};
|
EpdFont epdFont{&stubData};
|
||||||
@@ -241,7 +272,8 @@ class SdCardFont {
|
|||||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||||
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||||
template <typename Iter>
|
template <typename Iter>
|
||||||
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask);
|
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||||
|
const char* extraText = nullptr);
|
||||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||||
|
|
||||||
// Global helpers
|
// Global helpers
|
||||||
|
|||||||
@@ -34,19 +34,15 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender
|
|||||||
unloadAll(renderer);
|
unloadAll(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select by ordinal position: sort available sizes, then map the font size
|
// Select the physical point size closest to the built-in reader sizes. Some
|
||||||
// enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
|
// CJK font packs only ship larger sizes, so ordinal selection can make
|
||||||
// family has fewer sizes than 4, clamp to the last available size.
|
// MEDIUM load 18pt+ and produce oversized pages on small devices.
|
||||||
auto sizes = family.availableSizes();
|
const SdCardFontFileInfo* selected = family.findClosestReaderSize(fontSizeEnum);
|
||||||
if (sizes.empty()) {
|
if (!selected) {
|
||||||
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
|
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t idx = fontSizeEnum;
|
|
||||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
|
||||||
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
|
|
||||||
|
|
||||||
auto* font = new (std::nothrow) SdCardFont();
|
auto* font = new (std::nothrow) SdCardFont();
|
||||||
if (!font) {
|
if (!font) {
|
||||||
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
|
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
|
||||||
@@ -92,6 +88,14 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
|
|||||||
loadedPointSize_ = 0;
|
loadedPointSize_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t SdCardFontManager::reportMemory() const {
|
||||||
|
size_t total = 0;
|
||||||
|
for (const auto& lf : loaded_) {
|
||||||
|
if (lf.font) total += lf.font->reportMemory();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
int SdCardFontManager::getFontId(const std::string& familyName) const {
|
int SdCardFontManager::getFontId(const std::string& familyName) const {
|
||||||
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
|
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
|
||||||
return loaded_.front().fontId;
|
return loaded_.front().fontId;
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ class SdCardFontManager {
|
|||||||
SdCardFontManager(const SdCardFontManager&) = delete;
|
SdCardFontManager(const SdCardFontManager&) = delete;
|
||||||
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
|
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
|
||||||
|
|
||||||
// Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
|
// Load the font file whose physical point size is closest to the reader
|
||||||
// ordinal position in the family's sorted size list. Only one .cpfont file
|
// fontSizeEnum (SMALL=12, MEDIUM=14, LARGE=16, EXTRA_LARGE=18). Only one
|
||||||
// is loaded; other sizes remain on disk. This keeps resident interval +
|
// .cpfont file is loaded; other sizes remain on disk. This keeps resident
|
||||||
// kern/ligature tables to one size's worth of memory.
|
// interval + kern/ligature tables to one size's worth of memory.
|
||||||
// Returns true on success.
|
// Returns true on success.
|
||||||
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
|
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
|
||||||
|
|
||||||
@@ -32,7 +32,11 @@ class SdCardFontManager {
|
|||||||
// Get name of currently loaded family (empty if none).
|
// Get name of currently loaded family (empty if none).
|
||||||
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
||||||
|
|
||||||
// Point size that was actually loaded (closest match to targetPtSize).
|
// MEMFIX-PORT: font manager audit passthrough; portable
|
||||||
|
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
|
||||||
|
size_t reportMemory() const;
|
||||||
|
|
||||||
|
// Point size that was actually loaded.
|
||||||
// 0 if nothing loaded.
|
// 0 if nothing loaded.
|
||||||
uint8_t currentPointSize() const { return loadedPointSize_; };
|
uint8_t currentPointSize() const { return loadedPointSize_; };
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,60 @@ const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t s
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum,
|
||||||
|
const uint8_t style) const {
|
||||||
|
if (files.empty()) return nullptr;
|
||||||
|
|
||||||
|
// Collect sizes matching the requested style, sorted ascending.
|
||||||
|
std::vector<uint8_t> sizes;
|
||||||
|
for (const auto& f : files) {
|
||||||
|
if (f.style != style) continue;
|
||||||
|
sizes.push_back(f.pointSize);
|
||||||
|
}
|
||||||
|
if (sizes.empty()) return nullptr;
|
||||||
|
std::sort(sizes.begin(), sizes.end());
|
||||||
|
|
||||||
|
// When the family provides at least 4 sizes, use ordinal (index-based)
|
||||||
|
// selection so custom-built font sets (e.g. 10/12/14/16) map SMALL to
|
||||||
|
// the smallest file, not to a hardcoded 12pt target.
|
||||||
|
if (sizes.size() >= 4) {
|
||||||
|
uint8_t idx = fontSizeEnum;
|
||||||
|
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||||
|
return findFile(sizes[idx], style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fewer sizes than enum slots (e.g. CJK packs with only 2-3 sizes):
|
||||||
|
// fall back to closest-match against the built-in reader targets.
|
||||||
|
uint8_t target = 14;
|
||||||
|
switch (fontSizeEnum) {
|
||||||
|
case 0:
|
||||||
|
target = 12;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
target = 16;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
target = 18;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
target = 14;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SdCardFontFileInfo* best = nullptr;
|
||||||
|
uint8_t bestDelta = 255;
|
||||||
|
for (const auto& f : files) {
|
||||||
|
if (f.style != style) continue;
|
||||||
|
const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize;
|
||||||
|
if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) {
|
||||||
|
best = &f;
|
||||||
|
bestDelta = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
|
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
|
||||||
for (const auto& f : files) {
|
for (const auto& f : files) {
|
||||||
if (f.pointSize == size) return true;
|
if (f.pointSize == size) return true;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct SdCardFontFamilyInfo {
|
|||||||
std::vector<SdCardFontFileInfo> files;
|
std::vector<SdCardFontFileInfo> files;
|
||||||
|
|
||||||
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
|
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
|
||||||
|
const SdCardFontFileInfo* findClosestReaderSize(uint8_t fontSizeEnum, uint8_t style = 0) const;
|
||||||
bool hasSize(uint8_t size) const;
|
bool hasSize(uint8_t size) const;
|
||||||
std::vector<uint8_t> availableSizes() const;
|
std::vector<uint8_t> availableSizes() const;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
|||||||
!NotoSerif/**
|
!NotoSerif/**
|
||||||
!NotoSans/
|
!NotoSans/
|
||||||
!NotoSans/**
|
!NotoSans/**
|
||||||
|
!NotoSansArabic/
|
||||||
|
!NotoSansArabic/**
|
||||||
!NotoSansHebrew/
|
!NotoSansHebrew/
|
||||||
!NotoSansHebrew/**
|
!NotoSansHebrew/**
|
||||||
!OpenDyslexic/
|
!OpenDyslexic/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
+1872
-1256
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2244
-1483
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -49,17 +50,44 @@ INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
|
|||||||
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
|
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
|
||||||
|
|
||||||
|
|
||||||
def download_font(url: str, dest: Path) -> Path:
|
_orig_getaddrinfo = socket.getaddrinfo
|
||||||
"""Download a font file if not already cached. Returns the local path."""
|
|
||||||
|
|
||||||
|
def _ipv4_only_getaddrinfo(*args, **kwargs):
|
||||||
|
"""getaddrinfo variant that drops AAAA records (IPv4 only)."""
|
||||||
|
return [ai for ai in _orig_getaddrinfo(*args, **kwargs) if ai[0] == socket.AF_INET]
|
||||||
|
|
||||||
|
|
||||||
|
def download_font(url: str, dest: Path, retries: int = 3) -> Path:
|
||||||
|
"""Download a font file if not already cached. Returns the local path.
|
||||||
|
|
||||||
|
Some sources (e.g. mirrors.ctan.org) are round-robin redirectors that land
|
||||||
|
on a different mirror each request; a mirror may advertise an IPv6 address a
|
||||||
|
host without an IPv6 route cannot reach ([Errno 101] Network is unreachable).
|
||||||
|
Retry on failure, forcing IPv4 resolution after the first attempt.
|
||||||
|
"""
|
||||||
if dest.exists():
|
if dest.exists():
|
||||||
return dest
|
return dest
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
print(f" Downloading {dest.name}...")
|
print(f" Downloading {dest.name}...")
|
||||||
try:
|
last_err = None
|
||||||
urllib.request.urlretrieve(url, dest)
|
for attempt in range(1, retries + 1):
|
||||||
except Exception as e:
|
force_ipv4 = attempt > 1
|
||||||
dest.unlink(missing_ok=True)
|
if force_ipv4:
|
||||||
raise RuntimeError(f"Failed to download {url}: {e}") from e
|
socket.getaddrinfo = _ipv4_only_getaddrinfo
|
||||||
|
try:
|
||||||
|
urllib.request.urlretrieve(url, dest)
|
||||||
|
break
|
||||||
|
except Exception as e: # noqa: BLE001 - reported via RuntimeError below
|
||||||
|
last_err = e
|
||||||
|
dest.unlink(missing_ok=True)
|
||||||
|
if attempt < retries:
|
||||||
|
print(f" Attempt {attempt} failed ({e}); retrying (IPv4-only)...")
|
||||||
|
finally:
|
||||||
|
if force_ipv4:
|
||||||
|
socket.getaddrinfo = _orig_getaddrinfo
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Failed to download {url}: {last_err}") from last_err
|
||||||
size_kb = dest.stat().st_size / 1024
|
size_kb = dest.stat().st_size / 1024
|
||||||
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
|
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
|
||||||
return dest
|
return dest
|
||||||
|
|||||||
@@ -31,19 +31,48 @@ done
|
|||||||
UI_FONT_SIZES=(10 12)
|
UI_FONT_SIZES=(10 12)
|
||||||
UI_FONT_STYLES=("Regular" "Bold")
|
UI_FONT_STYLES=("Regular" "Bold")
|
||||||
|
|
||||||
|
# Arabic glyphs for UI text (menus, file browser titles). The built-in fonts
|
||||||
|
# must cover the *output* of MiniBidi's do_shape() — contextual presentation
|
||||||
|
# forms — not base letters, or shaped UI text silently drops glyphs.
|
||||||
|
# Curated for firmware-size budget: core Arabic (Presentation Forms-B,
|
||||||
|
# incl. the Lam-Alef ligature forms) plus the Farsi/Urdu extra letters'
|
||||||
|
# Presentation Forms-A blocks, the few characters shaping leaves at their
|
||||||
|
# base codepoint, Arabic punctuation, and both digit sets. No harakat and
|
||||||
|
# no Sindhi/Pashto/Kurdish forms — book text gets those from SD-card fonts.
|
||||||
|
ARABIC_INTERVALS=(
|
||||||
|
--additional-intervals 0x060C,0x060C # Arabic comma
|
||||||
|
--additional-intervals 0x061B,0x061B # Arabic semicolon
|
||||||
|
--additional-intervals 0x061F,0x061F # Arabic question mark
|
||||||
|
--additional-intervals 0x0621,0x0621 # hamza (non-joining, never shaped)
|
||||||
|
--additional-intervals 0x0640,0x0640 # tatweel
|
||||||
|
--additional-intervals 0x0660,0x0669 # Arabic-Indic digits
|
||||||
|
--additional-intervals 0x06BA,0x06BA # noon ghunna base (initial/medial keep base cp)
|
||||||
|
--additional-intervals 0x06D4,0x06D4 # Urdu full stop
|
||||||
|
--additional-intervals 0x06F0,0x06F9 # extended Arabic-Indic digits (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFB56,0xFB59 # peh (Farsi)
|
||||||
|
--additional-intervals 0xFB66,0xFB69 # tteh (Urdu)
|
||||||
|
--additional-intervals 0xFB7A,0xFB7D # tcheh (Farsi)
|
||||||
|
--additional-intervals 0xFB88,0xFB95 # ddal, jeh, rreh (Urdu), keheh, gaf (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFB9E,0xFB9F # noon ghunna isolated/final (Urdu)
|
||||||
|
--additional-intervals 0xFBA6,0xFBB1 # heh goal, heh doachashmee, yeh barree(+hamza) (Urdu)
|
||||||
|
--additional-intervals 0xFBFC,0xFBFF # farsi yeh (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFE80,0xFEFC # Presentation Forms-B: core Arabic + Lam-Alef
|
||||||
|
)
|
||||||
|
|
||||||
for size in ${UI_FONT_SIZES[@]}; do
|
for size in ${UI_FONT_SIZES[@]}; do
|
||||||
for style in ${UI_FONT_STYLES[@]}; do
|
for style in ${UI_FONT_STYLES[@]}; do
|
||||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
||||||
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
||||||
|
arabic_path="../builtinFonts/source/NotoSansArabic/NotoSansArabic-${style}.ttf"
|
||||||
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
||||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
||||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
# are filled from it while every glyph Ubuntu already has stays unchanged
|
||||||
# (fontstack is ordered by descending priority).
|
# (fontstack is ordered by descending priority).
|
||||||
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||||
output_path="../builtinFonts/${font_name}.h"
|
output_path="../builtinFonts/${font_name}.h"
|
||||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path $viet_path \
|
||||||
--additional-intervals 0x05D0,0x05EA > $output_path
|
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path
|
||||||
echo "Generated $output_path"
|
echo "Generated $output_path"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -51,7 +80,8 @@ done
|
|||||||
python fontconvert.py notosans_8_regular 8 \
|
python fontconvert.py notosans_8_regular 8 \
|
||||||
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
||||||
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
||||||
--additional-intervals 0x05D0,0x05EA > ../builtinFonts/notosans_8_regular.h
|
../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf \
|
||||||
|
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > ../builtinFonts/notosans_8_regular.h
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Running compression verification..."
|
echo "Running compression verification..."
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ unmerged_intervals = sorted(intervals + add_ints)
|
|||||||
intervals = []
|
intervals = []
|
||||||
unvalidated_intervals = []
|
unvalidated_intervals = []
|
||||||
for i_start, i_end in unmerged_intervals:
|
for i_start, i_end in unmerged_intervals:
|
||||||
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
|
if len(unvalidated_intervals) > 0 and i_start <= unvalidated_intervals[-1][1] + 1:
|
||||||
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
|
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
|
||||||
continue
|
continue
|
||||||
unvalidated_intervals.append((i_start, i_end))
|
unvalidated_intervals.append((i_start, i_end))
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ INTERVAL_PRESETS = {
|
|||||||
"ascii": [(0x0020, 0x007E)],
|
"ascii": [(0x0020, 0x007E)],
|
||||||
"latin1": [(0x0080, 0x00FF)],
|
"latin1": [(0x0080, 0x00FF)],
|
||||||
"latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F),
|
"latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F),
|
||||||
(0x1E00, 0x1EFF), (0x2000, 0x206F), (0xFB00, 0xFB06)],
|
(0x02B0, 0x02FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
|
||||||
|
(0xFB00, 0xFB06)],
|
||||||
"greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)],
|
"greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)],
|
||||||
"cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)],
|
"cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)],
|
||||||
"hebrew": [(0x0590, 0x05FF), (0xFB1D, 0xFB4F)],
|
"hebrew": [(0x0590, 0x05FF), (0xFB1D, 0xFB4F)],
|
||||||
@@ -62,7 +63,7 @@ INTERVAL_PRESETS = {
|
|||||||
# Composite preset for English-language literary fiction including scifi/popsci.
|
# Composite preset for English-language literary fiction including scifi/popsci.
|
||||||
# Greek for physics terms, math operators, geometric shapes, uncommon
|
# Greek for physics terms, math operators, geometric shapes, uncommon
|
||||||
# dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats.
|
# dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats.
|
||||||
"reading": [(0x0020, 0x024F), (0x0300, 0x036F), (0x0370, 0x03FF),
|
"reading": [(0x0020, 0x024F), (0x02B0, 0x02FF), (0x0300, 0x036F), (0x0370, 0x03FF),
|
||||||
(0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
|
(0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
|
||||||
(0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F),
|
(0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F),
|
||||||
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
|
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
|
||||||
|
|||||||
@@ -133,6 +133,15 @@ families:
|
|||||||
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
|
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
|
||||||
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
|
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
|
||||||
|
|
||||||
|
- name: Vollkorn
|
||||||
|
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
|
||||||
|
intervals: latin-ext,greek,cyrillic
|
||||||
|
sizes: [12, 14, 16, 18]
|
||||||
|
styles:
|
||||||
|
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
|
||||||
|
bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 700}}
|
||||||
|
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 400}}
|
||||||
|
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
|
||||||
|
|
||||||
# ── Sans-serif ─────────────────────────────────────────────────────────
|
# ── Sans-serif ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
+23
-61
@@ -5,6 +5,7 @@
|
|||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <PngToBmpConverter.h>
|
#include <PngToBmpConverter.h>
|
||||||
|
#include <Utf8.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
@@ -73,8 +74,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab data from opfParser into epub
|
// Grab data from opfParser into epub. Normalize titles to NFC so NFD (combining
|
||||||
bookMetadata.title = opfParser.title;
|
// mark) text renders correctly — the device fonts have no mark positioning.
|
||||||
|
bookMetadata.title = utf8ComposeNfc(opfParser.title);
|
||||||
bookMetadata.author = opfParser.author;
|
bookMetadata.author = opfParser.author;
|
||||||
bookMetadata.language = opfParser.language;
|
bookMetadata.language = opfParser.language;
|
||||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
@@ -151,18 +153,11 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
|
|
||||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||||
|
|
||||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
size_t ncxSize;
|
||||||
HalFile tempNcxFile;
|
if (!getItemSize(tocNcxItem, &ncxSize)) {
|
||||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
LOG_ERR("EBP", "Could not get size of toc ncx file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
|
||||||
// Explicitly close() file before reopening for reading
|
|
||||||
tempNcxFile.close();
|
|
||||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto ncxSize = tempNcxFile.size();
|
|
||||||
|
|
||||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
@@ -171,29 +166,13 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
// Stream the decompressed NCX straight into the parser instead of round-tripping
|
||||||
if (!ncxBuffer) {
|
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read toc ncx file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (tempNcxFile.available()) {
|
|
||||||
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
|
||||||
if (readSize == 0) break;
|
|
||||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
|
||||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
|
||||||
free(ncxBuffer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(ncxBuffer);
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
tempNcxFile.close();
|
|
||||||
Storage.remove(tmpNcxPath.c_str());
|
|
||||||
|
|
||||||
LOG_DBG("EBP", "Parsed TOC items");
|
LOG_DBG("EBP", "Parsed TOC items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -207,18 +186,11 @@ bool Epub::parseTocNavFile() const {
|
|||||||
|
|
||||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||||
|
|
||||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
size_t navSize;
|
||||||
HalFile tempNavFile;
|
if (!getItemSize(tocNavItem, &navSize)) {
|
||||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
LOG_ERR("EBP", "Could not get size of toc nav file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
|
||||||
// Explicitly close() file before reopening for reading
|
|
||||||
tempNavFile.close();
|
|
||||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto navSize = tempNavFile.size();
|
|
||||||
|
|
||||||
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||||
// and the HTMLX nav file will have hrefs relative to itself
|
// and the HTMLX nav file will have hrefs relative to itself
|
||||||
@@ -230,28 +202,13 @@ bool Epub::parseTocNavFile() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
// Stream the decompressed nav document straight into the parser instead of round-tripping
|
||||||
if (!navBuffer) {
|
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read toc nav file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (tempNavFile.available()) {
|
|
||||||
const auto readSize = tempNavFile.read(navBuffer, 1024);
|
|
||||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
|
||||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
|
||||||
free(navBuffer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(navBuffer);
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
tempNavFile.close();
|
|
||||||
Storage.remove(tmpNavPath.c_str());
|
|
||||||
|
|
||||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -394,6 +351,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
Storage.removeDir((cachePath + "/sections").c_str());
|
Storage.removeDir((cachePath + "/sections").c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Release the resolved CSS rule map: it is only needed transiently while building
|
||||||
|
// section caches, and createSectionFile reloads it from cache on demand. Holding it
|
||||||
|
// resident pins tens of KB for the whole reading session (more on warm resume into
|
||||||
|
// an already-cached chapter, where createSectionFile never runs to clear it).
|
||||||
|
cssParser->clear();
|
||||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ class Epub {
|
|||||||
}
|
}
|
||||||
~Epub() = default;
|
~Epub() = default;
|
||||||
std::string& getBasePath() { return contentBasePath; }
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
|
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
|
||||||
|
// Approximate resident heap of the open book (audit): path strings, the CSS
|
||||||
|
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
|
||||||
|
// (counts + HalFile handles) and contributes little.
|
||||||
|
size_t residentBytes() const {
|
||||||
|
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
|
||||||
|
contentBasePath.capacity() + cachePath.capacity();
|
||||||
|
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
|
||||||
|
if (cssParser) total += cssParser->residentBytes();
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
|
||||||
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "BookMetadataCache.h"
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
|
#include <BufferedFile.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
#include <Utf8.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
@@ -9,10 +11,56 @@
|
|||||||
#include "FsHelpers.h"
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t BOOK_CACHE_VERSION = 7;
|
constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-composed
|
||||||
constexpr char bookBinFile[] = "/book.bin";
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on
|
||||||
|
// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash.
|
||||||
|
constexpr size_t BUILD_IO_BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
// Entry (de)serializers, templated so they run over HalFile and the Buffered*
|
||||||
|
// wrappers alike (two instantiations each -- a few hundred bytes of flash, in
|
||||||
|
// exchange for the build path streaming at SD speed instead of per-pod).
|
||||||
|
template <typename F>
|
||||||
|
uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writePod(file, entry.cumulativeSize);
|
||||||
|
serialization::writePod(file, entry.tocIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.title);
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writeString(file, entry.anchor);
|
||||||
|
serialization::writePod(file, entry.level);
|
||||||
|
serialization::writePod(file, entry.spineIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) {
|
||||||
|
BookMetadataCache::SpineEntry entry;
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readPod(file, entry.cumulativeSize);
|
||||||
|
serialization::readPod(file, entry.tocIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
BookMetadataCache::TocEntry readTocEntryFrom(F& file) {
|
||||||
|
BookMetadataCache::TocEntry entry;
|
||||||
|
serialization::readString(file, entry.title);
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readString(file, entry.anchor);
|
||||||
|
serialization::readPod(file, entry.level);
|
||||||
|
serialization::readPod(file, entry.spineIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
@@ -29,13 +77,23 @@ bool BookMetadataCache::beginContentOpfPass() {
|
|||||||
LOG_DBG("BMC", "Beginning content opf pass");
|
LOG_DBG("BMC", "Beginning content opf pass");
|
||||||
|
|
||||||
// Open spine file for writing
|
// Open spine file for writing
|
||||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes.
|
||||||
|
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endContentOpfPass() {
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
|
const bool flushed = !passOut || passOut->flush();
|
||||||
|
passOut.reset();
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
return true;
|
if (!flushed) {
|
||||||
|
LOG_ERR("BMC", "Failed writing spine tmp file");
|
||||||
|
}
|
||||||
|
return flushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::beginTocPass() {
|
bool BookMetadataCache::beginTocPass() {
|
||||||
@@ -73,10 +131,17 @@ bool BookMetadataCache::beginTocPass() {
|
|||||||
useSpineHrefIndex = false;
|
useSpineHrefIndex = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
|
||||||
|
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endTocPass() {
|
bool BookMetadataCache::endTocPass() {
|
||||||
|
const bool flushed = !passOut || passOut->flush();
|
||||||
|
passOut.reset();
|
||||||
|
if (!flushed) {
|
||||||
|
LOG_ERR("BMC", "Failed writing toc tmp file");
|
||||||
|
}
|
||||||
// Explicit close() required: member variables persist beyond function scope
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
@@ -85,7 +150,7 @@ bool BookMetadataCache::endTocPass() {
|
|||||||
spineHrefIndex.shrink_to_fit();
|
spineHrefIndex.shrink_to_fit();
|
||||||
useSpineHrefIndex = false;
|
useSpineHrefIndex = false;
|
||||||
|
|
||||||
return true;
|
return flushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endWrite() {
|
bool BookMetadataCache::endWrite() {
|
||||||
@@ -118,6 +183,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffered streams for the whole build: every access below is sequential per
|
||||||
|
// file, but interleaved ACROSS files, which thrashes SdFat's single shared
|
||||||
|
// sector cache when unbuffered (one 512B SD transaction per 4-byte pod --
|
||||||
|
// measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return.
|
||||||
|
serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
|
||||||
constexpr uint32_t headerASize =
|
constexpr uint32_t headerASize =
|
||||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||||
@@ -127,31 +200,34 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
const uint32_t lutOffset = headerASize + metadataSize;
|
const uint32_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
// Header A
|
// Header A
|
||||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
serialization::writePod(bookOut, BOOK_CACHE_VERSION);
|
||||||
serialization::writePod(bookFile, lutOffset);
|
serialization::writePod(bookOut, lutOffset);
|
||||||
serialization::writePod(bookFile, spineCount);
|
serialization::writePod(bookOut, spineCount);
|
||||||
serialization::writePod(bookFile, tocCount);
|
serialization::writePod(bookOut, tocCount);
|
||||||
// Metadata
|
// Metadata
|
||||||
serialization::writeString(bookFile, metadata.title);
|
serialization::writeString(bookOut, metadata.title);
|
||||||
serialization::writeString(bookFile, metadata.author);
|
serialization::writeString(bookOut, metadata.author);
|
||||||
serialization::writeString(bookFile, metadata.language);
|
serialization::writeString(bookOut, metadata.language);
|
||||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
serialization::writeString(bookOut, metadata.coverItemHref);
|
||||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
serialization::writeString(bookOut, metadata.textReferenceHref);
|
||||||
|
|
||||||
// Loop through spine entries, writing LUT positions
|
// Loop through spine entries, writing LUT positions
|
||||||
spineFile.seek(0);
|
spineIn.seek(0);
|
||||||
for (int i = 0; i < spineCount; i++) {
|
for (int i = 0; i < spineCount; i++) {
|
||||||
uint32_t pos = spineFile.position();
|
const uint32_t pos = spineIn.position();
|
||||||
auto spineEntry = readSpineEntry(spineFile);
|
readSpineEntryFrom(spineIn);
|
||||||
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
serialization::writePod(bookOut, pos + lutOffset + lutSize);
|
||||||
}
|
}
|
||||||
|
// Total size of the spine tmp file: entries land in book.bin after the toc LUT
|
||||||
|
// and the full spine block, so toc LUT positions are offset by it.
|
||||||
|
const auto spineBytes = static_cast<uint32_t>(spineIn.position());
|
||||||
|
|
||||||
// Loop through toc entries, writing LUT positions
|
// Loop through toc entries, writing LUT positions
|
||||||
tocFile.seek(0);
|
tocIn.seek(0);
|
||||||
for (int i = 0; i < tocCount; i++) {
|
for (int i = 0; i < tocCount; i++) {
|
||||||
uint32_t pos = tocFile.position();
|
const uint32_t pos = tocIn.position();
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
readTocEntryFrom(tocIn);
|
||||||
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LUTs complete
|
// LUTs complete
|
||||||
@@ -159,9 +235,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
|
|
||||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||||
tocFile.seek(0);
|
tocIn.seek(0);
|
||||||
for (int j = 0; j < tocCount; j++) {
|
for (int j = 0; j < tocCount; j++) {
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
auto tocEntry = readTocEntryFrom(tocIn);
|
||||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||||
@@ -196,9 +272,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
std::deque<ZipFile::SizeTarget> targets;
|
std::deque<ZipFile::SizeTarget> targets;
|
||||||
targets.resize(spineCount);
|
targets.resize(spineCount);
|
||||||
|
|
||||||
spineFile.seek(0);
|
spineIn.seek(0);
|
||||||
for (int i = 0; i < spineCount; i++) {
|
for (int i = 0; i < spineCount; i++) {
|
||||||
auto entry = readSpineEntry(spineFile);
|
auto entry = readSpineEntryFrom(spineIn);
|
||||||
std::string path = FsHelpers::normalisePath(entry.href);
|
std::string path = FsHelpers::normalisePath(entry.href);
|
||||||
|
|
||||||
ZipFile::SizeTarget t;
|
ZipFile::SizeTarget t;
|
||||||
@@ -223,10 +299,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t cumSize = 0;
|
uint32_t cumSize = 0;
|
||||||
spineFile.seek(0);
|
spineIn.seek(0);
|
||||||
int lastSpineTocIndex = -1;
|
int lastSpineTocIndex = -1;
|
||||||
for (int i = 0; i < spineCount; i++) {
|
for (int i = 0; i < spineCount; i++) {
|
||||||
auto spineEntry = readSpineEntry(spineFile);
|
auto spineEntry = readSpineEntryFrom(spineIn);
|
||||||
|
|
||||||
spineEntry.tocIndex = spineToTocIndex[i];
|
spineEntry.tocIndex = spineToTocIndex[i];
|
||||||
|
|
||||||
@@ -259,23 +335,33 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
spineEntry.cumulativeSize = cumSize;
|
spineEntry.cumulativeSize = cumSize;
|
||||||
|
|
||||||
// Write out spine data to book.bin
|
// Write out spine data to book.bin
|
||||||
writeSpineEntry(bookFile, spineEntry);
|
writeSpineEntryTo(bookOut, spineEntry);
|
||||||
}
|
}
|
||||||
// Close opened zip file
|
// Close opened zip file
|
||||||
zip.close();
|
zip.close();
|
||||||
|
|
||||||
// Loop through toc entries from toc file writing to book.bin
|
// Loop through toc entries from toc file writing to book.bin
|
||||||
tocFile.seek(0);
|
tocIn.seek(0);
|
||||||
for (int i = 0; i < tocCount; i++) {
|
for (int i = 0; i < tocCount; i++) {
|
||||||
auto tocEntry = readTocEntry(tocFile);
|
auto tocEntry = readTocEntryFrom(tocIn);
|
||||||
writeTocEntry(bookFile, tocEntry);
|
writeTocEntryTo(bookOut, tocEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool written = bookOut.flush();
|
||||||
|
|
||||||
// Explicit close() required: member variables persist beyond function scope
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
|
|
||||||
|
if (!written) {
|
||||||
|
// A short write (card full/removed) would leave a truncated book.bin that
|
||||||
|
// still passes the version check on load; remove it so the next open rebuilds.
|
||||||
|
LOG_ERR("BMC", "Failed writing book.bin, removing truncated file");
|
||||||
|
Storage.remove((cachePath + bookBinFile).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DBG("BMC", "Successfully built book.bin");
|
LOG_DBG("BMC", "Successfully built book.bin");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -293,21 +379,11 @@ bool BookMetadataCache::cleanupTmpFiles() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||||
const uint32_t pos = file.position();
|
return writeSpineEntryTo(file, entry);
|
||||||
serialization::writeString(file, entry.href);
|
|
||||||
serialization::writePod(file, entry.cumulativeSize);
|
|
||||||
serialization::writePod(file, entry.tocIndex);
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||||
const uint32_t pos = file.position();
|
return writeTocEntryTo(file, entry);
|
||||||
serialization::writeString(file, entry.title);
|
|
||||||
serialization::writeString(file, entry.href);
|
|
||||||
serialization::writeString(file, entry.anchor);
|
|
||||||
serialization::writePod(file, entry.level);
|
|
||||||
serialization::writePod(file, entry.spineIndex);
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||||
@@ -319,7 +395,11 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SpineEntry entry(href, 0, -1);
|
const SpineEntry entry(href, 0, -1);
|
||||||
writeSpineEntry(spineFile, entry);
|
if (passOut) {
|
||||||
|
writeSpineEntryTo(*passOut, entry);
|
||||||
|
} else {
|
||||||
|
writeSpineEntry(spineFile, entry);
|
||||||
|
}
|
||||||
spineCount++;
|
spineCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,8 +444,14 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TocEntry entry(title, href, anchor, level, spineIndex);
|
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
||||||
writeTocEntry(tocFile, entry);
|
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||||
|
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||||
|
if (passOut) {
|
||||||
|
writeTocEntryTo(*passOut, entry);
|
||||||
|
} else {
|
||||||
|
writeTocEntry(tocFile, entry);
|
||||||
|
}
|
||||||
tocCount++;
|
tocCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,19 +525,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||||
SpineEntry entry;
|
return readSpineEntryFrom(file);
|
||||||
serialization::readString(file, entry.href);
|
|
||||||
serialization::readPod(file, entry.cumulativeSize);
|
|
||||||
serialization::readPod(file, entry.tocIndex);
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
|
||||||
TocEntry entry;
|
|
||||||
serialization::readString(file, entry.title);
|
|
||||||
serialization::readString(file, entry.href);
|
|
||||||
serialization::readString(file, entry.anchor);
|
|
||||||
serialization::readPod(file, entry.level);
|
|
||||||
serialization::readPod(file, entry.spineIndex);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <BufferedFile.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class BookMetadataCache {
|
class BookMetadataCache {
|
||||||
@@ -54,6 +56,11 @@ class BookMetadataCache {
|
|||||||
// Temp file handles during build
|
// Temp file handles during build
|
||||||
HalFile spineFile;
|
HalFile spineFile;
|
||||||
HalFile tocFile;
|
HalFile tocFile;
|
||||||
|
// Buffers the per-entry tmp-file writes during the OPF/TOC passes: those
|
||||||
|
// writes interleave with zip-inflate SD reads, and unbuffered they thrash
|
||||||
|
// SdFat's shared sector cache (one 512B transaction per 4-byte pod). One
|
||||||
|
// wrapper serves whichever pass is active (spine, then toc).
|
||||||
|
std::unique_ptr<serialization::BufferedFileWriter> passOut;
|
||||||
|
|
||||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||||
struct SpineHrefIndexEntry {
|
struct SpineHrefIndexEntry {
|
||||||
|
|||||||
+55
-3
@@ -6,6 +6,20 @@
|
|||||||
|
|
||||||
#include <new>
|
#include <new>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
template <typename Predicate>
|
||||||
|
void renderFilteredPageElements(const std::vector<std::shared_ptr<PageElement>>& elements, GfxRenderer& renderer,
|
||||||
|
const int fontId, const int xOffset, const int yOffset, Predicate&& predicate) {
|
||||||
|
for (const auto& element : elements) {
|
||||||
|
if (predicate(*element)) {
|
||||||
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
||||||
}
|
}
|
||||||
@@ -25,7 +39,17 @@ std::unique_ptr<PageLine> PageLine::deserialize(HalFile& file) {
|
|||||||
serialization::readPod(file, yPos);
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
auto tb = TextBlock::deserialize(file);
|
auto tb = TextBlock::deserialize(file);
|
||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
if (!tb) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: null TextBlock");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* line = new (std::nothrow) PageLine(std::move(tb), xPos, yPos);
|
||||||
|
if (!line) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: could not allocate PageLine");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::unique_ptr<PageLine>(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
@@ -33,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
|
|||||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
|
||||||
|
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
bool PageImage::serialize(HalFile& file) {
|
bool PageImage::serialize(HalFile& file) {
|
||||||
serialization::writePod(file, xPos);
|
serialization::writePod(file, xPos);
|
||||||
serialization::writePod(file, yPos);
|
serialization::writePod(file, yPos);
|
||||||
@@ -93,8 +121,22 @@ std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(HalFile& fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
for (auto& element : elements) {
|
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, [](const PageElement&) { return true; });
|
||||||
element->render(renderer, fontId, xOffset, yOffset);
|
}
|
||||||
|
|
||||||
|
void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
|
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset,
|
||||||
|
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
|
||||||
|
const int yOffset) const {
|
||||||
|
for (const auto& element : elements) {
|
||||||
|
if (element->getTag() == TAG_PageImage) {
|
||||||
|
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
|
||||||
|
} else {
|
||||||
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +173,10 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
|
|||||||
|
|
||||||
uint16_t count;
|
uint16_t count;
|
||||||
serialization::readPod(file, count);
|
serialization::readPod(file, count);
|
||||||
|
// Reserve up front: growth-by-doubling needs old + new capacity live at once and
|
||||||
|
// reallocates repeatedly — a field crash (bad_alloc -> abort under -fno-exceptions)
|
||||||
|
// hit exactly this append path on a heavily fragmented heap.
|
||||||
|
page->elements.reserve(count);
|
||||||
|
|
||||||
for (uint16_t i = 0; i < count; i++) {
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
uint8_t tag;
|
uint8_t tag;
|
||||||
@@ -138,9 +184,15 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
|
|||||||
|
|
||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(file);
|
auto pl = PageLine::deserialize(file);
|
||||||
|
if (!pl) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
} else if (tag == TAG_PageImage) {
|
} else if (tag == TAG_PageImage) {
|
||||||
auto pi = PageImage::deserialize(file);
|
auto pi = PageImage::deserialize(file);
|
||||||
|
if (!pi) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
page->elements.push_back(std::move(pi));
|
page->elements.push_back(std::move(pi));
|
||||||
} else if (tag == TAG_PageHorizontalRule) {
|
} else if (tag == TAG_PageHorizontalRule) {
|
||||||
auto rule = PageHorizontalRule::deserialize(file);
|
auto rule = PageHorizontalRule::deserialize(file);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
|
|||||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
|
||||||
bool serialize(HalFile& file) override;
|
bool serialize(HalFile& file) override;
|
||||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
||||||
@@ -88,6 +89,8 @@ class Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
bool serialize(HalFile& file) const;
|
bool serialize(HalFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(HalFile& file);
|
static std::unique_ptr<Page> deserialize(HalFile& file);
|
||||||
|
|
||||||
@@ -97,6 +100,13 @@ class Page {
|
|||||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasImagesNeedingDecode() const {
|
||||||
|
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
|
||||||
|
return element->getTag() == TAG_PageImage &&
|
||||||
|
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Get bounding box of all images on the page (union of image rects)
|
// Get bounding box of all images on the page (union of image rects)
|
||||||
// Returns false if no images. Coordinates are relative to page origin.
|
// Returns false if no images. Coordinates are relative to page origin.
|
||||||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||||
|
|||||||
+274
-39
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -24,6 +25,7 @@ constexpr size_t RTL_PARAGRAPH_PROBE_WORDS = 3;
|
|||||||
// Per-word: scan enough chars to see through leading neutrals (quotes, numbers)
|
// Per-word: scan enough chars to see through leading neutrals (quotes, numbers)
|
||||||
// before giving up. 64 is a hedge for pathological cases like long numeric tokens.
|
// before giving up. 64 is a hedge for pathological cases like long numeric tokens.
|
||||||
constexpr int RTL_PER_WORD_PROBE_DEPTH = 64;
|
constexpr int RTL_PER_WORD_PROBE_DEPTH = 64;
|
||||||
|
constexpr size_t MIN_JUSTIFY_GAPS = 1;
|
||||||
|
|
||||||
// Byte-level pre-check: Hebrew UTF-8 lead bytes 0xD6-0xD7, Arabic/Syriac 0xD8-0xDB.
|
// Byte-level pre-check: Hebrew UTF-8 lead bytes 0xD6-0xD7, Arabic/Syriac 0xD8-0xDB.
|
||||||
bool mayContainRtlBytes(const char* str) {
|
bool mayContainRtlBytes(const char* str) {
|
||||||
@@ -57,6 +59,134 @@ uint32_t lastCodepoint(const std::string& word) {
|
|||||||
|
|
||||||
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
||||||
|
|
||||||
|
bool isNoBreakBeforeCjkPunctuation(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '.':
|
||||||
|
case ',':
|
||||||
|
case ':':
|
||||||
|
case ';':
|
||||||
|
case '!':
|
||||||
|
case '?':
|
||||||
|
case ')':
|
||||||
|
case ']':
|
||||||
|
case '}':
|
||||||
|
case 0x00BB: // »
|
||||||
|
case 0x2019: // ’
|
||||||
|
case 0x201D: // ”
|
||||||
|
case 0x3001: // 、
|
||||||
|
case 0x3002: // 。
|
||||||
|
case 0x3009: // 〉
|
||||||
|
case 0x300B: // 》
|
||||||
|
case 0x300D: // 」
|
||||||
|
case 0x300F: // 』
|
||||||
|
case 0x3011: // 】
|
||||||
|
case 0x3015: // 〕
|
||||||
|
case 0x3017: // 〗
|
||||||
|
case 0x3019: // 〙
|
||||||
|
case 0x301B: // 〛
|
||||||
|
case 0xFF01: // !
|
||||||
|
case 0xFF09: // )
|
||||||
|
case 0xFF0C: // ,
|
||||||
|
case 0xFF0E: // .
|
||||||
|
case 0xFF1A: // :
|
||||||
|
case 0xFF1B: // ;
|
||||||
|
case 0xFF1F: // ?
|
||||||
|
case 0xFF3D: // ]
|
||||||
|
case 0xFF5D: // }
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isNoBreakAfterCjkPunctuation(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '(':
|
||||||
|
case '[':
|
||||||
|
case '{':
|
||||||
|
case 0x00AB: // «
|
||||||
|
case 0x2018: // ‘
|
||||||
|
case 0x201C: // “
|
||||||
|
case 0x3008: // 〈
|
||||||
|
case 0x300A: // 《
|
||||||
|
case 0x300C: // 「
|
||||||
|
case 0x300E: // 『
|
||||||
|
case 0x3010: // 【
|
||||||
|
case 0x3014: // 〔
|
||||||
|
case 0x3016: // 〖
|
||||||
|
case 0x3018: // 〘
|
||||||
|
case 0x301A: // 〚
|
||||||
|
case 0xFF08: // (
|
||||||
|
case 0xFF3B: // [
|
||||||
|
case 0xFF5B: // {
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsCjkBreakableCodepoint(const std::string& text) {
|
||||||
|
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
|
||||||
|
while (*ptr) {
|
||||||
|
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||||
|
if (utf8IsCjkBreakable(cp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) {
|
||||||
|
if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false;
|
||||||
|
if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false;
|
||||||
|
if (utf8IsCombiningMark(rightCp)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<size_t> cjkCharacterBreakByteOffsets(const std::string& text) {
|
||||||
|
struct CodepointBoundary {
|
||||||
|
uint32_t cp;
|
||||||
|
size_t endOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<CodepointBoundary> codepoints;
|
||||||
|
codepoints.reserve(text.size());
|
||||||
|
bool hasCjkBreakable = false;
|
||||||
|
|
||||||
|
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
|
||||||
|
const auto* const start = ptr;
|
||||||
|
while (*ptr) {
|
||||||
|
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||||
|
if (cp == 0) break;
|
||||||
|
if (utf8IsCjkBreakable(cp)) {
|
||||||
|
hasCjkBreakable = true;
|
||||||
|
}
|
||||||
|
codepoints.push_back({cp, static_cast<size_t>(ptr - start)});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCjkBreakable || codepoints.size() < 2) return {};
|
||||||
|
|
||||||
|
std::vector<size_t> allowedOffsets;
|
||||||
|
allowedOffsets.reserve(codepoints.size() - 1);
|
||||||
|
for (size_t i = 0; i + 1 < codepoints.size(); ++i) {
|
||||||
|
const uint32_t current = codepoints[i].cp;
|
||||||
|
const uint32_t next = codepoints[i + 1].cp;
|
||||||
|
if (!hasCjkBreakOpportunityBetween(current, next)) continue;
|
||||||
|
allowedOffsets.push_back(codepoints[i].endOffset);
|
||||||
|
}
|
||||||
|
return allowedOffsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
int computeJustifyExtra(const int spareSpace, const size_t gapCount) {
|
||||||
|
if (gapCount < MIN_JUSTIFY_GAPS || spareSpace <= 0) return 0;
|
||||||
|
// Distribute the spare space evenly across gaps. Do NOT bail out to 0 when the
|
||||||
|
// per-gap stretch is large: a sparse line (few words on a wide page) legitimately
|
||||||
|
// needs big gaps to reach the margin. Returning 0 there disables justification for
|
||||||
|
// that line, leaving it right-aligned (RTL) / left-aligned (LTR) — the mismatched
|
||||||
|
// alignment bug. Match the un-capped behavior of the old code.
|
||||||
|
return spareSpace / static_cast<int>(gapCount);
|
||||||
|
}
|
||||||
|
|
||||||
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
||||||
void stripSoftHyphensInPlace(std::string& word) {
|
void stripSoftHyphensInPlace(std::string& word) {
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
@@ -125,6 +255,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
const bool attachToPrevious) {
|
const bool attachToPrevious) {
|
||||||
if (word.empty()) return;
|
if (word.empty()) return;
|
||||||
|
|
||||||
|
// The device fonts carry no combining-mark positioning, so EPUB text stored in NFD
|
||||||
|
// (a base letter followed by separate combining accents -- common for Vietnamese,
|
||||||
|
// and used for many EPUB <h1> chapter headings) renders with the marks detached or
|
||||||
|
// misplaced. Compose to NFC here, the single funnel every word passes through, so a
|
||||||
|
// precomposed glyph is used instead. This runs once per word at layout time (the
|
||||||
|
// result is cached in the section file) and is a cheap no-op for mark-free text.
|
||||||
|
word = utf8ComposeNfc(word);
|
||||||
|
|
||||||
EpdFontFamily::Style baseStyle = fontStyle;
|
EpdFontFamily::Style baseStyle = fontStyle;
|
||||||
if (underline) {
|
if (underline) {
|
||||||
baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE);
|
baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE);
|
||||||
@@ -132,12 +270,54 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) &&
|
const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) &&
|
||||||
BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH);
|
BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH);
|
||||||
|
|
||||||
|
const auto pushToken = [&](std::string token, const bool continues, const bool noSpaceBefore,
|
||||||
|
const bool isFocusSuffix) {
|
||||||
|
words.push_back(std::move(token));
|
||||||
|
wordStyles.push_back(baseStyle);
|
||||||
|
wordContinues.push_back(continues);
|
||||||
|
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||||
|
wordIsFocusSuffix.push_back(isFocusSuffix);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool effectiveAttachToPrevious = attachToPrevious;
|
||||||
|
bool effectiveNoSpaceBefore = false;
|
||||||
|
if (attachToPrevious && !words.empty() &&
|
||||||
|
hasCjkBreakOpportunityBetween(lastCodepoint(words.back()), firstCodepoint(word))) {
|
||||||
|
effectiveAttachToPrevious = false;
|
||||||
|
effectiveNoSpaceBefore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
|
||||||
|
bool firstToken = true;
|
||||||
|
size_t tokenStart = 0;
|
||||||
|
for (const size_t breakOffset : breakOffsets) {
|
||||||
|
if (breakOffset <= tokenStart || breakOffset > word.size()) continue;
|
||||||
|
pushToken(word.substr(tokenStart, breakOffset - tokenStart), firstToken ? effectiveAttachToPrevious : false,
|
||||||
|
firstToken ? effectiveNoSpaceBefore : true, false);
|
||||||
|
firstToken = false;
|
||||||
|
tokenStart = breakOffset;
|
||||||
|
}
|
||||||
|
if (tokenStart < word.size()) {
|
||||||
|
pushToken(word.substr(tokenStart), firstToken ? effectiveAttachToPrevious : false,
|
||||||
|
firstToken ? effectiveNoSpaceBefore : true, false);
|
||||||
|
}
|
||||||
|
if (wordStartsRtl) {
|
||||||
|
hasRtlWord = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsCjkBreakableCodepoint(word)) {
|
||||||
|
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
|
||||||
|
if (wordStartsRtl) {
|
||||||
|
hasRtlWord = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
|
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
|
||||||
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
|
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
|
||||||
words.push_back(std::move(word));
|
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
|
||||||
wordStyles.push_back(baseStyle);
|
|
||||||
wordContinues.push_back(attachToPrevious);
|
|
||||||
wordIsFocusSuffix.push_back(false);
|
|
||||||
if (wordStartsRtl) {
|
if (wordStartsRtl) {
|
||||||
hasRtlWord = true;
|
hasRtlWord = true;
|
||||||
}
|
}
|
||||||
@@ -166,17 +346,19 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
words.reserve(newCapacity);
|
words.reserve(newCapacity);
|
||||||
wordStyles.reserve(newCapacity);
|
wordStyles.reserve(newCapacity);
|
||||||
wordContinues.reserve(newCapacity);
|
wordContinues.reserve(newCapacity);
|
||||||
|
wordNoSpaceBefore.reserve(newCapacity);
|
||||||
wordIsFocusSuffix.reserve(newCapacity);
|
wordIsFocusSuffix.reserve(newCapacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lambda helper to process and push individual sub-segments of the string
|
// Lambda helper to process and push individual sub-segments of the string
|
||||||
// Use std::string_view to avoid heap allocations when slicing
|
// Use std::string_view to avoid heap allocations when slicing
|
||||||
auto processSegment = [&](std::string_view segment, bool isWord, bool attach) {
|
auto processSegment = [&](std::string_view segment, bool isWord, bool attach, bool noSpaceBefore) {
|
||||||
if (!isWord) {
|
if (!isWord) {
|
||||||
// Punctuation and Numbers stay regular
|
// Punctuation and Numbers stay regular
|
||||||
words.emplace_back(segment);
|
words.emplace_back(segment);
|
||||||
wordStyles.push_back(baseStyle);
|
wordStyles.push_back(baseStyle);
|
||||||
wordContinues.push_back(attach);
|
wordContinues.push_back(attach);
|
||||||
|
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||||
wordIsFocusSuffix.push_back(false);
|
wordIsFocusSuffix.push_back(false);
|
||||||
} else {
|
} else {
|
||||||
size_t charCount = 0;
|
size_t charCount = 0;
|
||||||
@@ -198,6 +380,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
words.emplace_back(segment);
|
words.emplace_back(segment);
|
||||||
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
||||||
wordContinues.push_back(attach);
|
wordContinues.push_back(attach);
|
||||||
|
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||||
wordIsFocusSuffix.push_back(false);
|
wordIsFocusSuffix.push_back(false);
|
||||||
} else {
|
} else {
|
||||||
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
|
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
|
||||||
@@ -210,12 +393,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
words.emplace_back(segment.substr(0, splitByteOffset));
|
words.emplace_back(segment.substr(0, splitByteOffset));
|
||||||
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
||||||
wordContinues.push_back(attach);
|
wordContinues.push_back(attach);
|
||||||
|
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||||
wordIsFocusSuffix.push_back(false);
|
wordIsFocusSuffix.push_back(false);
|
||||||
|
|
||||||
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry
|
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry
|
||||||
words.emplace_back(segment.substr(splitByteOffset));
|
words.emplace_back(segment.substr(splitByteOffset));
|
||||||
wordStyles.push_back(baseStyle);
|
wordStyles.push_back(baseStyle);
|
||||||
wordContinues.push_back(true);
|
wordContinues.push_back(true);
|
||||||
|
wordNoSpaceBefore.push_back(false);
|
||||||
wordIsFocusSuffix.push_back(true);
|
wordIsFocusSuffix.push_back(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,7 +428,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
|
|
||||||
// Only the very first segment inherits the original attachToPrevious flag.
|
// Only the very first segment inherits the original attachToPrevious flag.
|
||||||
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
|
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
|
||||||
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
|
processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
|
||||||
|
isFirstSegment ? effectiveNoSpaceBefore : false);
|
||||||
|
|
||||||
// Setup for the next segment
|
// Setup for the next segment
|
||||||
segmentStart = currentCpStart;
|
segmentStart = currentCpStart;
|
||||||
@@ -255,7 +441,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
// Process the final remaining segment
|
// Process the final remaining segment
|
||||||
size_t segmentLen = end - segmentStart;
|
size_t segmentLen = end - segmentStart;
|
||||||
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
|
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
|
||||||
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
|
processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
|
||||||
|
isFirstSegment ? effectiveNoSpaceBefore : false);
|
||||||
if (wordStartsRtl) {
|
if (wordStartsRtl) {
|
||||||
hasRtlWord = true;
|
hasRtlWord = true;
|
||||||
}
|
}
|
||||||
@@ -324,14 +511,16 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
std::vector<size_t> lineBreakIndices;
|
std::vector<size_t> lineBreakIndices;
|
||||||
if (hyphenationEnabled) {
|
if (hyphenationEnabled) {
|
||||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
lineBreakIndices =
|
||||||
|
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
|
||||||
} else {
|
} else {
|
||||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
|
||||||
}
|
}
|
||||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
for (size_t i = 0; i < lineCount; ++i) {
|
for (size_t i = 0; i < lineCount; ++i) {
|
||||||
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
extractLine(i, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore, lineBreakIndices, processLine, renderer,
|
||||||
|
fontId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove consumed words so size() reflects only remaining words
|
// Remove consumed words so size() reflects only remaining words
|
||||||
@@ -340,6 +529,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
words.erase(words.begin(), words.begin() + consumed);
|
words.erase(words.begin(), words.begin() + consumed);
|
||||||
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
||||||
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
||||||
|
wordNoSpaceBefore.erase(wordNoSpaceBefore.begin(), wordNoSpaceBefore.begin() + consumed);
|
||||||
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
|
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +546,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
std::vector<bool>& noSpaceBeforeVec) {
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -395,7 +586,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
|||||||
for (size_t j = i; j < totalWordCount; ++j) {
|
for (size_t j = i; j < totalWordCount; ++j) {
|
||||||
// Add space before word j, unless it's the first word on the line or a continuation
|
// Add space before word j, unless it's the first word on the line or a continuation
|
||||||
int gap = 0;
|
int gap = 0;
|
||||||
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
if (j > static_cast<size_t>(i) && noSpaceBeforeVec[j]) {
|
||||||
|
gap = 0;
|
||||||
|
} else if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
||||||
gap =
|
gap =
|
||||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||||
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
||||||
@@ -470,7 +663,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
|||||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||||
const int pageWidth, std::vector<uint16_t>& wordWidths,
|
const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||||
std::vector<bool>& continuesVec) {
|
std::vector<bool>& continuesVec,
|
||||||
|
std::vector<bool>& noSpaceBeforeVec) {
|
||||||
const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId);
|
const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId);
|
||||||
|
|
||||||
std::vector<size_t> lineBreakIndices;
|
std::vector<size_t> lineBreakIndices;
|
||||||
@@ -488,7 +682,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
|||||||
while (currentIndex < wordWidths.size()) {
|
while (currentIndex < wordWidths.size()) {
|
||||||
const bool isFirstWord = currentIndex == lineStart;
|
const bool isFirstWord = currentIndex == lineStart;
|
||||||
int spacing = 0;
|
int spacing = 0;
|
||||||
if (!isFirstWord && !continuesVec[currentIndex]) {
|
if (!isFirstWord && noSpaceBeforeVec[currentIndex]) {
|
||||||
|
spacing = 0;
|
||||||
|
} else if (!isFirstWord && !continuesVec[currentIndex]) {
|
||||||
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
|
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||||
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
||||||
@@ -618,6 +814,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
|||||||
// line, while "kilometer" moves to the next line.
|
// line, while "kilometer" moves to the next line.
|
||||||
// wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment.
|
// wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment.
|
||||||
wordContinues.insert(wordContinues.begin() + wordIndex + 1, false);
|
wordContinues.insert(wordContinues.begin() + wordIndex + 1, false);
|
||||||
|
wordNoSpaceBefore.insert(wordNoSpaceBefore.begin() + wordIndex + 1, false);
|
||||||
|
|
||||||
// Update cached widths to reflect the new prefix/remainder pairing.
|
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||||
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
||||||
@@ -627,7 +824,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
|
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
|
||||||
|
const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||||
const GfxRenderer& renderer, const int fontId) {
|
const GfxRenderer& renderer, const int fontId) {
|
||||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||||
@@ -660,7 +858,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||||
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
||||||
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||||
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
if (wordIdx > 0 && noSpaceBeforeVec[lastBreakAt + wordIdx]) {
|
||||||
|
// Unicode break opportunity with no inserted Latin-style space. It is still
|
||||||
|
// a stretchable gap for justified CJK/Korean text.
|
||||||
|
actualGapCount++;
|
||||||
|
} else if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||||
actualGapCount++;
|
actualGapCount++;
|
||||||
totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx - 1]),
|
totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx - 1]),
|
||||||
firstCodepoint(lineWords[wordIdx]), lineWordStyles[wordIdx - 1]);
|
firstCodepoint(lineWords[wordIdx]), lineWordStyles[wordIdx - 1]);
|
||||||
@@ -689,8 +891,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
|
|
||||||
// For justified text, compute per-gap extra to distribute remaining space evenly
|
// For justified text, compute per-gap extra to distribute remaining space evenly
|
||||||
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||||
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1)
|
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||||
? spareSpace / static_cast<int>(actualGapCount)
|
? computeJustifyExtra(spareSpace, actualGapCount)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// BiDi processing: reorder words with UAX#9 in full-line context.
|
// BiDi processing: reorder words with UAX#9 in full-line context.
|
||||||
@@ -709,11 +911,13 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
reorderedStylesScratch.clear();
|
reorderedStylesScratch.clear();
|
||||||
reorderedWidthsScratch.clear();
|
reorderedWidthsScratch.clear();
|
||||||
reorderedContinuesScratch.clear();
|
reorderedContinuesScratch.clear();
|
||||||
|
reorderedNoSpaceBeforeScratch.clear();
|
||||||
reorderedFocusSuffixScratch.clear();
|
reorderedFocusSuffixScratch.clear();
|
||||||
reorderedWordsScratch.reserve(visualOrderScratch.size());
|
reorderedWordsScratch.reserve(visualOrderScratch.size());
|
||||||
reorderedStylesScratch.reserve(visualOrderScratch.size());
|
reorderedStylesScratch.reserve(visualOrderScratch.size());
|
||||||
reorderedWidthsScratch.reserve(visualOrderScratch.size());
|
reorderedWidthsScratch.reserve(visualOrderScratch.size());
|
||||||
reorderedContinuesScratch.reserve(visualOrderScratch.size());
|
reorderedContinuesScratch.reserve(visualOrderScratch.size());
|
||||||
|
reorderedNoSpaceBeforeScratch.reserve(visualOrderScratch.size());
|
||||||
reorderedFocusSuffixScratch.reserve(visualOrderScratch.size());
|
reorderedFocusSuffixScratch.reserve(visualOrderScratch.size());
|
||||||
|
|
||||||
for (size_t i = 0; i < visualOrderScratch.size(); ++i) {
|
for (size_t i = 0; i < visualOrderScratch.size(); ++i) {
|
||||||
@@ -740,6 +944,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
reorderedContinuesScratch.push_back(continues);
|
reorderedContinuesScratch.push_back(continues);
|
||||||
|
reorderedNoSpaceBeforeScratch.push_back(!continues && noSpaceBeforeVec[lastBreakAt + src]);
|
||||||
}
|
}
|
||||||
|
|
||||||
int reorderedWordWidthSum = 0;
|
int reorderedWordWidthSum = 0;
|
||||||
@@ -747,7 +952,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
int reorderedNaturalGaps = 0;
|
int reorderedNaturalGaps = 0;
|
||||||
for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) {
|
for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) {
|
||||||
reorderedWordWidthSum += reorderedWidthsScratch[wordIdx];
|
reorderedWordWidthSum += reorderedWidthsScratch[wordIdx];
|
||||||
if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
|
if (wordIdx > 0 && reorderedNoSpaceBeforeScratch[wordIdx]) {
|
||||||
|
// Unicode break opportunity with no inserted Latin-style space. It is still
|
||||||
|
// a stretchable gap for justified CJK/Korean text.
|
||||||
|
reorderedGapCount++;
|
||||||
|
} else if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
|
||||||
reorderedGapCount++;
|
reorderedGapCount++;
|
||||||
reorderedNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx - 1]),
|
reorderedNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx - 1]),
|
||||||
firstCodepoint(reorderedWordsScratch[wordIdx]),
|
firstCodepoint(reorderedWordsScratch[wordIdx]),
|
||||||
@@ -763,10 +972,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
|
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
|
||||||
const int reorderedJustifyExtra =
|
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||||
(effectiveAlignment == CssTextAlign::Justify && !isLastLine && reorderedGapCount >= 1)
|
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
|
||||||
? reorderedSpare / static_cast<int>(reorderedGapCount)
|
: 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||||
? reorderedJustifyExtra * static_cast<int>(reorderedGapCount)
|
? reorderedJustifyExtra * static_cast<int>(reorderedGapCount)
|
||||||
@@ -799,15 +1007,20 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
int advance =
|
int advance =
|
||||||
renderer.getKerning(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
|
renderer.getKerning(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
|
||||||
firstCodepoint(reorderedWordsScratch[wordIdx + 1]), reorderedStylesScratch[wordIdx]);
|
firstCodepoint(reorderedWordsScratch[wordIdx + 1]), reorderedStylesScratch[wordIdx]);
|
||||||
if (reorderedWordsScratch[wordIdx] == " " && reorderedContinuesScratch[wordIdx] &&
|
// wordIdx > 0 mirrors the gap accounting above (which skips index 0): a leading
|
||||||
|
// no-break space must not receive justifyExtra, or the line over-stretches by one
|
||||||
|
// gap and the last word is pushed past the right margin (issue #2185).
|
||||||
|
if (wordIdx > 0 && reorderedWordsScratch[wordIdx] == " " && reorderedContinuesScratch[wordIdx] &&
|
||||||
effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||||
advance += reorderedJustifyExtra;
|
advance += reorderedJustifyExtra;
|
||||||
}
|
}
|
||||||
xpos += advance;
|
xpos += advance;
|
||||||
} else if (wordIdx + 1 < reorderedWidthsScratch.size()) {
|
} else if (wordIdx + 1 < reorderedWidthsScratch.size()) {
|
||||||
int gap = renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
|
const bool nextNoSpace = reorderedNoSpaceBeforeScratch[wordIdx + 1];
|
||||||
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
|
int gap = nextNoSpace ? 0
|
||||||
reorderedStylesScratch[wordIdx]);
|
: renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
|
||||||
|
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
|
||||||
|
reorderedStylesScratch[wordIdx]);
|
||||||
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||||
gap += reorderedJustifyExtra;
|
gap += reorderedJustifyExtra;
|
||||||
}
|
}
|
||||||
@@ -839,18 +1052,23 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
// Cross-boundary kerning for continuation words
|
// Cross-boundary kerning for continuation words
|
||||||
int advance = renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
|
int advance = renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||||
if (lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
|
// wordIdx > 0: see the LTR branch — a leading no-break space is not a justifiable gap.
|
||||||
|
if (wordIdx > 0 && lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
|
||||||
effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||||
advance += justifyExtra;
|
advance += justifyExtra;
|
||||||
}
|
}
|
||||||
xpos -= advance;
|
xpos -= advance;
|
||||||
} else {
|
} else {
|
||||||
int gap = 0;
|
int gap = 0;
|
||||||
|
bool nextNoSpace = false;
|
||||||
if (wordIdx + 1 < lineWordCount) {
|
if (wordIdx + 1 < lineWordCount) {
|
||||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
|
||||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
gap = nextNoSpace
|
||||||
|
? 0
|
||||||
|
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||||
|
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||||
}
|
}
|
||||||
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||||
gap += justifyExtra;
|
gap += justifyExtra;
|
||||||
}
|
}
|
||||||
xpos -= gap;
|
xpos -= gap;
|
||||||
@@ -873,18 +1091,25 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
int advance = wordWidths[lastBreakAt + wordIdx];
|
int advance = wordWidths[lastBreakAt + wordIdx];
|
||||||
advance += renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
|
advance += renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||||
if (lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
|
// wordIdx > 0 mirrors the gap accounting above (which skips index 0): a leading
|
||||||
|
// no-break space must not receive justifyExtra, or the line over-stretches by one
|
||||||
|
// gap and the last word is pushed past the right margin (issue #2185).
|
||||||
|
if (wordIdx > 0 && lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
|
||||||
effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||||
advance += justifyExtra;
|
advance += justifyExtra;
|
||||||
}
|
}
|
||||||
xpos += advance;
|
xpos += advance;
|
||||||
} else {
|
} else {
|
||||||
int gap = 0;
|
int gap = 0;
|
||||||
|
bool nextNoSpace = false;
|
||||||
if (wordIdx + 1 < lineWordCount) {
|
if (wordIdx + 1 < lineWordCount) {
|
||||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
|
||||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
gap = nextNoSpace
|
||||||
|
? 0
|
||||||
|
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||||
|
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||||
}
|
}
|
||||||
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||||
gap += justifyExtra;
|
gap += justifyExtra;
|
||||||
}
|
}
|
||||||
xpos += wordWidths[lastBreakAt + wordIdx] + gap;
|
xpos += wordWidths[lastBreakAt + wordIdx] + gap;
|
||||||
@@ -909,8 +1134,14 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!lineHasFocusSplit) {
|
if (!lineHasFocusSplit) {
|
||||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
|
// TextBlock flattens the vectors into its arena; they stay owned here and die at return.
|
||||||
std::vector<uint8_t>{}, std::vector<uint16_t>{}, blockStyle));
|
auto block = std::make_shared<TextBlock>(lineWords, lineXPos, lineWordStyles, std::vector<uint8_t>{},
|
||||||
|
std::vector<uint16_t>{}, blockStyle);
|
||||||
|
if (!block->valid()) {
|
||||||
|
LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processLine(std::move(block));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,6 +1186,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processLine(std::make_shared<TextBlock>(std::move(outWords), std::move(outXPos), std::move(outStyles),
|
auto block = std::make_shared<TextBlock>(outWords, outXPos, outStyles, outBoundaries, outSuffixX, blockStyle);
|
||||||
std::move(outBoundaries), std::move(outSuffixX), blockStyle));
|
if (!block->valid()) {
|
||||||
|
LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processLine(std::move(block));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class GfxRenderer;
|
|||||||
class ParsedText {
|
class ParsedText {
|
||||||
std::vector<std::string> words;
|
std::vector<std::string> words;
|
||||||
std::vector<EpdFontFamily::Style> wordStyles;
|
std::vector<EpdFontFamily::Style> wordStyles;
|
||||||
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
|
std::vector<bool> wordContinues; // true = word attaches to previous with no break
|
||||||
|
std::vector<bool> wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined
|
||||||
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
|
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
@@ -27,18 +28,22 @@ class ParsedText {
|
|||||||
std::vector<EpdFontFamily::Style> reorderedStylesScratch;
|
std::vector<EpdFontFamily::Style> reorderedStylesScratch;
|
||||||
std::vector<uint16_t> reorderedWidthsScratch;
|
std::vector<uint16_t> reorderedWidthsScratch;
|
||||||
std::vector<bool> reorderedContinuesScratch;
|
std::vector<bool> reorderedContinuesScratch;
|
||||||
|
std::vector<bool> reorderedNoSpaceBeforeScratch;
|
||||||
std::vector<bool> reorderedFocusSuffixScratch;
|
std::vector<bool> reorderedFocusSuffixScratch;
|
||||||
std::vector<uint16_t> visualOrderScratch;
|
std::vector<uint16_t> visualOrderScratch;
|
||||||
|
|
||||||
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
|
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
|
||||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
std::vector<bool>& noSpaceBeforeVec);
|
||||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
std::vector<bool>& noSpaceBeforeVec);
|
||||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||||
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
|
||||||
|
const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
||||||
int fontId);
|
int fontId);
|
||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|||||||
+525
-111
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "Epub/css/CssParser.h"
|
#include "Epub/css/CssParser.h"
|
||||||
@@ -10,33 +11,68 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 26;
|
// v28: text decoration bits now include line-through in serialized wordStyles.
|
||||||
|
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||||
|
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||||
|
// v30: Arabic shaping changed both drawing and measurement (getTextAdvanceX now
|
||||||
|
// measures the shaped visual text); cached word positions from v29 no longer
|
||||||
|
// match what drawText renders.
|
||||||
|
constexpr uint8_t SECTION_FILE_VERSION = 30;
|
||||||
|
// Written into the version field while a build is in progress; patched to
|
||||||
|
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||||
|
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||||
|
// as unknown and clears -- so an incomplete file is never mistaken for a valid one.
|
||||||
|
constexpr uint8_t SECTION_FILE_INCOMPLETE_VERSION = 0;
|
||||||
|
// Written when a build is suspended partway (reader exited or device slept mid-build).
|
||||||
|
// The file carries valid pages 0..pageCount-1, all LUTs, and a trailer with the parse
|
||||||
|
// watermark (bytesConsumed, totalBytes) appended after the li LUT. loadSectionFile
|
||||||
|
// accepts it so a resume shows those pages instantly; the reader extends it by
|
||||||
|
// rebuilding in the background. Uses the same header layout as SECTION_FILE_VERSION,
|
||||||
|
// so finalized files are untouched by this feature; older firmware treats the sentinel
|
||||||
|
// as an unknown version and rebuilds, which is a safe downgrade.
|
||||||
|
// MUST change in lockstep with SECTION_FILE_VERSION: the sentinel IS the partial's
|
||||||
|
// format version, so a stale-format partial otherwise passes the header check and
|
||||||
|
// only fails (noisily, via the block-decode error path) when a page is loaded.
|
||||||
|
// Derived so the pairing can't be forgotten: 0xFE for v28, 0xFD for v29, ...
|
||||||
|
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE - (SECTION_FILE_VERSION - 28);
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||||
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||||
sizeof(uint32_t) + sizeof(uint32_t);
|
sizeof(uint32_t) + sizeof(uint32_t);
|
||||||
|
|
||||||
struct PageLutEntry {
|
|
||||||
uint32_t fileOffset;
|
|
||||||
uint16_t paragraphIndex;
|
|
||||||
uint16_t listItemIndex;
|
|
||||||
};
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
|
||||||
|
// constructed/destroyed where the parser's full definition is visible.
|
||||||
|
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
|
: epub(epub),
|
||||||
|
spineIndex(spineIndex),
|
||||||
|
renderer(renderer),
|
||||||
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
|
|
||||||
|
// Suspend any in-progress build so every section.reset() / navigation / sleep path
|
||||||
|
// persists the pages already laid out as a partial .bin instead of discarding them
|
||||||
|
// (no-op once a build has completed or never started).
|
||||||
|
Section::~Section() { suspendBuild(); }
|
||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t position = file.position();
|
const uint32_t position = file.position();
|
||||||
if (!page->serialize(file)) {
|
if (!page->serialize(file)) {
|
||||||
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
|
LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
LOG_DBG("SCT", "Page %d processed", pageCount);
|
LOG_DBG("SCT", "Page %d processed", builtPageCount_);
|
||||||
|
|
||||||
pageCount++;
|
builtPageCount_++;
|
||||||
|
// pageCount is the pages available to read: a rebuild over a partial only raises it
|
||||||
|
// once it has laid out more pages than the partial already covers.
|
||||||
|
if (builtPageCount_ > pageCount) {
|
||||||
|
pageCount = builtPageCount_;
|
||||||
|
}
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +91,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
|||||||
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
||||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||||
"Header size mismatch");
|
"Header size mismatch");
|
||||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
// Written as the incomplete sentinel; finalizeBuild() patches it to
|
||||||
|
// SECTION_FILE_VERSION as the last step, committing the file.
|
||||||
|
serialization::writePod(file, SECTION_FILE_INCOMPLETE_VERSION);
|
||||||
serialization::writePod(file, fontId);
|
serialization::writePod(file, fontId);
|
||||||
serialization::writePod(file, lineCompression);
|
serialization::writePod(file, lineCompression);
|
||||||
serialization::writePod(file, extraParagraphSpacing);
|
serialization::writePod(file, extraParagraphSpacing);
|
||||||
@@ -82,16 +120,18 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Match parameters
|
// Match parameters
|
||||||
|
bool filePartial = false;
|
||||||
{
|
{
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != SECTION_FILE_VERSION) {
|
if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) {
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
file.close();
|
file.close();
|
||||||
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
int fileFontId;
|
int fileFontId;
|
||||||
uint16_t fileViewportWidth, fileViewportHeight;
|
uint16_t fileViewportWidth, fileViewportHeight;
|
||||||
@@ -126,14 +166,42 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
serialization::readPod(file, pageCount);
|
serialization::readPod(file, pageCount);
|
||||||
|
|
||||||
|
if (filePartial) {
|
||||||
|
// A partial's pageCount is the watermark of a suspended build. Read the watermark
|
||||||
|
// trailer (appended after the li LUT) so estimatedTotalPages can extrapolate.
|
||||||
|
uint32_t liLutOffset = 0;
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||||
|
serialization::readPod(file, liLutOffset);
|
||||||
|
const uint32_t trailerOffset = liLutOffset + static_cast<uint32_t>(pageCount) * sizeof(uint16_t);
|
||||||
|
const bool trailerValid =
|
||||||
|
pageCount > 0 && liLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size();
|
||||||
|
if (!trailerValid) {
|
||||||
|
file.close();
|
||||||
|
LOG_ERR("SCT", "Deserialization failed: malformed partial section");
|
||||||
|
clearCache();
|
||||||
|
pageCount = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file.seek(trailerOffset);
|
||||||
|
serialization::readPod(file, partialBytesConsumed_);
|
||||||
|
serialization::readPod(file, partialTotalBytes_);
|
||||||
|
partial_ = true;
|
||||||
|
partialPageCount_ = pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
file.close();
|
file.close();
|
||||||
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
LOG_DBG("SCT", "Deserialization succeeded: %d pages%s", pageCount, filePartial ? " (partial)" : "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||||
bool Section::clearCache() const {
|
bool Section::clearCache() const {
|
||||||
|
const std::string tmpBin = binTmpPath();
|
||||||
|
if (Storage.exists(tmpBin.c_str())) {
|
||||||
|
Storage.remove(tmpBin.c_str());
|
||||||
|
}
|
||||||
if (!Storage.exists(filePath.c_str())) {
|
if (!Storage.exists(filePath.c_str())) {
|
||||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
@@ -153,8 +221,43 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
const uint8_t imageRendering, const bool focusReadingEnabled,
|
const uint8_t imageRendering, const bool focusReadingEnabled,
|
||||||
const std::function<void()>& popupFn) {
|
const std::function<void()>& popupFn) {
|
||||||
|
// One-shot build: start, then lay out the whole section in a single pass.
|
||||||
|
if (!startBuild(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
|
||||||
|
hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled, popupFn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!buildSomeMore(0)) { // 0 = build to completion
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return buildComplete_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::startBuild(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight,
|
||||||
|
const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering,
|
||||||
|
const bool focusReadingEnabled, const std::function<void()>& popupFn) {
|
||||||
|
if (build_) {
|
||||||
|
LOG_ERR("SCT", "startBuild called while a build is already active");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buildComplete_ = false;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
// Pages from a loaded partial stay readable (from filePath) while this build writes
|
||||||
|
// to the tmp .bin, so availability never drops below the partial's watermark.
|
||||||
|
pageCount = partial_ ? partialPageCount_ : 0;
|
||||||
|
|
||||||
|
// Remove a stale tmp .bin from a crash-interrupted build; this build recreates it.
|
||||||
|
{
|
||||||
|
const std::string staleTmp = binTmpPath();
|
||||||
|
if (Storage.exists(staleTmp.c_str())) {
|
||||||
|
Storage.remove(staleTmp.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
const auto htmlDir = epub->getCachePath() + "/html";
|
||||||
|
const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html";
|
||||||
|
const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
|
|
||||||
// Create cache directory if it doesn't exist
|
// Create cache directory if it doesn't exist
|
||||||
{
|
{
|
||||||
@@ -162,62 +265,101 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
Storage.mkdir(sectionsDir.c_str());
|
Storage.mkdir(sectionsDir.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry logic for SD card timing issues
|
// Reuse the previously unzipped HTML if we already have it. The unzipped HTML is keyed only on the
|
||||||
bool success = false;
|
// book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation
|
||||||
uint32_t fileSize = 0;
|
// that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip
|
||||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
// inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so
|
||||||
if (attempt > 0) {
|
// even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a
|
||||||
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
// reopen skip the multi-second inflate. If htmlPath exists it is known-complete.
|
||||||
delay(50); // Brief delay before retry
|
const bool reusedHtml = Storage.exists(htmlPath.c_str());
|
||||||
|
bool htmlCached = reusedHtml;
|
||||||
|
if (reusedHtml) {
|
||||||
|
LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str());
|
||||||
|
} else {
|
||||||
|
Storage.mkdir(htmlDir.c_str());
|
||||||
|
|
||||||
|
// Retry logic for SD card timing issues
|
||||||
|
bool streamed = false;
|
||||||
|
uint32_t fileSize = 0;
|
||||||
|
for (int attempt = 0; attempt < 3 && !streamed; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||||
|
delay(50); // Brief delay before retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any incomplete file from previous attempt before retrying
|
||||||
|
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile tmpHtml;
|
||||||
|
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Larger chunks mean far fewer SD writes inflating the HTML; a 1KB chunk turned a 584KB
|
||||||
|
// single-spine novel into ~570 tiny writes (multi-second). 8KB keeps the transient buffers
|
||||||
|
// small while cutting the write count 8x.
|
||||||
|
streamed = epub->readItemContentsToStream(localPath, tmpHtml, 8192);
|
||||||
|
fileSize = tmpHtml.size();
|
||||||
|
// Explicitly close() file before calling Storage.remove()
|
||||||
|
tmpHtml.close();
|
||||||
|
|
||||||
|
// If streaming failed, remove the incomplete file immediately
|
||||||
|
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any incomplete file from previous attempt before retrying
|
if (!streamed) {
|
||||||
if (Storage.exists(tmpHtmlPath.c_str())) {
|
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||||
Storage.remove(tmpHtmlPath.c_str());
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HalFile tmpHtml;
|
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||||
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
|
||||||
fileSize = tmpHtml.size();
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
tmpHtml.close();
|
|
||||||
|
|
||||||
// If streaming failed, remove the incomplete file immediately
|
// Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are
|
||||||
if (!success && Storage.exists(tmpHtmlPath.c_str())) {
|
// valid regardless of whether the layout build finishes, so reopening (even a window-only spine
|
||||||
Storage.remove(tmpHtmlPath.c_str());
|
// that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp.
|
||||||
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) {
|
||||||
|
htmlCached = true;
|
||||||
|
} else {
|
||||||
|
LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!Storage.openFileForWrite("SCT", binTmpPath(), file)) {
|
||||||
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
|
||||||
|
|
||||||
if (!Storage.openFileForWrite("SCT", filePath, file)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Header is written with the incomplete-version sentinel; finalizeBuild() commits it.
|
||||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
||||||
std::vector<PageLutEntry> lut = {};
|
|
||||||
|
auto ctx = makeUniqueNoThrow<BuildContext>();
|
||||||
|
if (!ctx) {
|
||||||
|
LOG_ERR("SCT", "OOM: BuildContext");
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// htmlCached == "htmlPath is the live cache" (reused, or just promoted). finalizeBuild/abandonBuild
|
||||||
|
// then leave the cached HTML alone; only an un-promoted temp (rename failed) is theirs to clean up.
|
||||||
|
ctx->reusedHtml = htmlCached;
|
||||||
|
ctx->htmlPath = htmlPath;
|
||||||
|
ctx->tmpHtmlPath = tmpHtmlPath;
|
||||||
|
ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath;
|
||||||
|
|
||||||
// Derive the content base directory and image cache path prefix for the parser
|
// Derive the content base directory and image cache path prefix for the parser
|
||||||
size_t lastSlash = localPath.find_last_of('/');
|
const size_t lastSlash = localPath.find_last_of('/');
|
||||||
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||||
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||||
|
|
||||||
CssParser* cssParser = nullptr;
|
|
||||||
if (embeddedStyle) {
|
if (embeddedStyle) {
|
||||||
cssParser = epub->getCssParser();
|
ctx->cssParser = epub->getCssParser();
|
||||||
if (cssParser) {
|
if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
|
||||||
if (!cssParser->loadFromCache()) {
|
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,113 +376,376 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
// The parser stores the path/contentBase/imageBasePath by reference, so they must
|
||||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
// live in the BuildContext (which outlives the parser). The page-complete callback
|
||||||
viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
// captures the BuildContext pointer to append to its in-RAM LUT; build_ owns the
|
||||||
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
// context for the parser's whole lifetime.
|
||||||
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
BuildContext* ctxPtr = ctx.get();
|
||||||
|
ctx->parser = makeUniqueNoThrow<ChapterHtmlSlimParser>(
|
||||||
|
epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
|
||||||
|
viewportWidth, viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
||||||
|
[this, ctxPtr](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
||||||
|
ctxPtr->lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
||||||
},
|
},
|
||||||
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser);
|
embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn,
|
||||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
ctxPtr->cssParser);
|
||||||
success = visitor.parseAndBuildPages();
|
if (!ctx->parser) {
|
||||||
|
LOG_ERR("SCT", "OOM: ChapterHtmlSlimParser");
|
||||||
Storage.remove(tmpHtmlPath.c_str());
|
if (ctx->cssParser) ctx->cssParser->clear();
|
||||||
if (!success) {
|
|
||||||
LOG_ERR("SCT", "Failed to parse XML and build pages");
|
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
file.close();
|
file.close();
|
||||||
Storage.remove(filePath.c_str());
|
Storage.remove(binTmpPath().c_str());
|
||||||
if (cssParser) {
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
cssParser->clear();
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
|
build_ = std::move(ctx);
|
||||||
|
|
||||||
|
if (!build_->parser->beginParse()) {
|
||||||
|
LOG_ERR("SCT", "Failed to begin parse");
|
||||||
|
abandonBuild();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
build_->totalBytes = build_->parser->parseTotalBytes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::buildSomeMore(const int maxPages) {
|
||||||
|
if (!build_ || !build_->parser) {
|
||||||
|
LOG_ERR("SCT", "buildSomeMore with no active build");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial,
|
||||||
|
// pageCount stays pinned at the partial's watermark until the build passes it, which
|
||||||
|
// would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark.
|
||||||
|
const int startCount = builtPageCount_;
|
||||||
|
for (;;) {
|
||||||
|
const auto status = build_->parser->parseStep();
|
||||||
|
if (status == ChapterHtmlSlimParser::ParseStatus::Error) {
|
||||||
|
LOG_ERR("SCT", "Parse error during incremental build");
|
||||||
|
abandonBuild();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (status == ChapterHtmlSlimParser::ParseStatus::Done) {
|
||||||
|
return finalizeBuild();
|
||||||
|
}
|
||||||
|
// ParseStatus::More: yield once we've laid out the requested number of pages.
|
||||||
|
if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) {
|
||||||
|
build_->bytesConsumed = build_->parser->parseBytesConsumed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::hasHtmlCache() const {
|
||||||
|
const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html";
|
||||||
|
return Storage.exists(htmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::findAnchorDuringBuild(const std::string& anchor) const {
|
||||||
|
if (!build_ || !build_->parser) return std::nullopt;
|
||||||
|
for (const auto& [key, page] : build_->parser->getAnchors()) {
|
||||||
|
if (key == anchor) return page;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::findAnchor(const std::string& anchor) const {
|
||||||
|
if (const auto page = findAnchorDuringBuild(anchor)) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
// Fall back to the on-disk anchor map: a finalized section, or a partial whose map
|
||||||
|
// covers everything up to its watermark (nullopt past it -- build further and retry).
|
||||||
|
return getPageForAnchor(anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Section::estimatedTotalPages() const {
|
||||||
|
// Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA
|
||||||
|
// damping is needed. Also the best guess while a rebuild is running but hasn't laid out
|
||||||
|
// enough pages yet to extrapolate from its own progress.
|
||||||
|
const auto partialEstimate = [this]() -> uint16_t {
|
||||||
|
if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) {
|
||||||
|
return pageCount;
|
||||||
|
}
|
||||||
|
const uint64_t est = static_cast<uint64_t>(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_;
|
||||||
|
if (est <= pageCount) return pageCount;
|
||||||
|
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!build_) {
|
||||||
|
return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact
|
||||||
|
}
|
||||||
|
const uint32_t consumed = build_->bytesConsumed;
|
||||||
|
const uint32_t total = build_->totalBytes;
|
||||||
|
if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate();
|
||||||
|
|
||||||
|
// Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This
|
||||||
|
// re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses
|
||||||
|
// dense vs sparse regions of the chapter.
|
||||||
|
const uint64_t raw = static_cast<uint64_t>(builtPageCount_) * total / consumed;
|
||||||
|
|
||||||
|
// Damp that jitter with an exponential moving average. Step it once per build advance (keyed on
|
||||||
|
// bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how
|
||||||
|
// often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so
|
||||||
|
// the average settles onto the true count (and finalizeBuild then returns the exact pageCount).
|
||||||
|
constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle
|
||||||
|
if (build_->smoothedEstimate <= 0) {
|
||||||
|
build_->smoothedEstimate = static_cast<float>(raw); // seed on the first estimate
|
||||||
|
} else if (consumed != build_->smoothedAtConsumed) {
|
||||||
|
build_->smoothedEstimate += ALPHA * (static_cast<float>(raw) - build_->smoothedEstimate);
|
||||||
|
}
|
||||||
|
build_->smoothedAtConsumed = consumed;
|
||||||
|
|
||||||
|
const uint64_t est = static_cast<uint64_t>(build_->smoothedEstimate + 0.5f);
|
||||||
|
if (est <= pageCount) return pageCount; // never fewer than the pages already available
|
||||||
|
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built
|
||||||
|
// page count and table offsets, stamp `version` as the commit point, then swap the tmp
|
||||||
|
// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer
|
||||||
|
// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate
|
||||||
|
// the total page count. The parser must still be alive (anchors are read from it).
|
||||||
|
// On failure the tmp is removed and any pre-existing file at filePath is left intact.
|
||||||
|
bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) {
|
||||||
|
const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
|
const auto failCommit = [this]() {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const uint32_t lutOffset = file.position();
|
const uint32_t lutOffset = file.position();
|
||||||
bool hasFailedLutRecords = false;
|
for (const auto& entry : build_->lut) {
|
||||||
// Write LUT
|
|
||||||
for (const auto& entry : lut) {
|
|
||||||
if (entry.fileOffset == 0) {
|
if (entry.fileOffset == 0) {
|
||||||
hasFailedLutRecords = true;
|
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||||
break;
|
return failCommit();
|
||||||
}
|
}
|
||||||
serialization::writePod(file, entry.fileOffset);
|
serialization::writePod(file, entry.fileOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFailedLutRecords) {
|
// Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a
|
||||||
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
// partial, skip anchors that landed on the incomplete trailing page the suspend drops.
|
||||||
// Explicitly close() file before calling Storage.remove()
|
|
||||||
file.close();
|
|
||||||
Storage.remove(filePath.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write anchor-to-page map for fragment navigation (e.g. footnote targets)
|
|
||||||
const uint32_t anchorMapOffset = file.position();
|
const uint32_t anchorMapOffset = file.position();
|
||||||
const auto& anchors = visitor.getAnchors();
|
const auto& anchors = build_->parser->getAnchors();
|
||||||
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
|
uint16_t anchorCount = 0;
|
||||||
for (const auto& [anchor, page] : anchors) {
|
for (const auto& [anchor, page] : anchors) {
|
||||||
|
if (!asPartial || page < builtPageCount_) anchorCount++;
|
||||||
|
}
|
||||||
|
serialization::writePod(file, anchorCount);
|
||||||
|
for (const auto& [anchor, page] : anchors) {
|
||||||
|
if (asPartial && page >= builtPageCount_) continue;
|
||||||
serialization::writeString(file, anchor);
|
serialization::writeString(file, anchor);
|
||||||
serialization::writePod(file, page);
|
serialization::writePod(file, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t paragraphLutOffset = file.position();
|
const uint32_t paragraphLutOffset = file.position();
|
||||||
serialization::writePod(file, static_cast<uint16_t>(lut.size()));
|
serialization::writePod(file, static_cast<uint16_t>(build_->lut.size()));
|
||||||
for (const auto& entry : lut) {
|
for (const auto& entry : build_->lut) {
|
||||||
serialization::writePod(file, entry.paragraphIndex);
|
serialization::writePod(file, entry.paragraphIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
|
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
|
||||||
for (const auto& entry : lut) {
|
for (const auto& entry : build_->lut) {
|
||||||
serialization::writePod(file, entry.listItemIndex);
|
serialization::writePod(file, entry.listItemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch header with final pageCount, lutOffset, anchorMapOffset, paragraphLutOffset, and liLutOffset
|
if (asPartial) {
|
||||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(pageCount));
|
// Watermark trailer, located on load as liLutOffset + pageCount * sizeof(uint16_t).
|
||||||
serialization::writePod(file, pageCount);
|
serialization::writePod(file, bytesConsumed);
|
||||||
|
serialization::writePod(file, totalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch header with the built page count and section offsets...
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(builtPageCount_));
|
||||||
|
serialization::writePod(file, builtPageCount_);
|
||||||
serialization::writePod(file, lutOffset);
|
serialization::writePod(file, lutOffset);
|
||||||
serialization::writePod(file, anchorMapOffset);
|
serialization::writePod(file, anchorMapOffset);
|
||||||
serialization::writePod(file, paragraphLutOffset);
|
serialization::writePod(file, paragraphLutOffset);
|
||||||
serialization::writePod(file, liLutFileOffset);
|
serialization::writePod(file, liLutFileOffset);
|
||||||
|
// ...then commit by overwriting the sentinel version with the real one. Writing the
|
||||||
|
// version last makes it the commit point: a crash before here leaves version 0.
|
||||||
|
file.seek(0);
|
||||||
|
serialization::writePod(file, version);
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
file.close();
|
file.close();
|
||||||
if (cssParser) {
|
|
||||||
cssParser->clear();
|
// Swap into place. A crash between remove and rename loses the old file but keeps a
|
||||||
|
// fully-committed tmp; the next build just removes it and rebuilds.
|
||||||
|
if (Storage.exists(filePath.c_str())) {
|
||||||
|
Storage.remove(filePath.c_str());
|
||||||
|
}
|
||||||
|
if (!Storage.rename(binTmpPath().c_str(), filePath.c_str())) {
|
||||||
|
LOG_ERR("SCT", "Failed to move built section into place");
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
bool Section::finalizeBuild() {
|
||||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
// Flush the trailing page (emits the last page via the completePageFn into the LUT).
|
||||||
|
build_->parser->finishParse();
|
||||||
|
|
||||||
|
if (!build_->reusedHtml) {
|
||||||
|
// Parse succeeded: promote the freshly unzipped HTML to the persistent cache so future
|
||||||
|
// rebuilds skip zip inflation. If promotion fails, drop the temp -- the build still succeeded.
|
||||||
|
if (!Storage.rename(build_->tmpHtmlPath.c_str(), build_->htmlPath.c_str())) {
|
||||||
|
LOG_DBG("SCT", "Failed to promote HTML cache, removing temp");
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0);
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
build_.reset();
|
||||||
|
if (!committed) {
|
||||||
|
// commitBuildFile removed filePath before the failed swap, so nothing valid remains.
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buildComplete_ = true;
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = builtPageCount_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::suspendBuild() {
|
||||||
|
if (!build_) return;
|
||||||
|
|
||||||
|
// Only worth persisting if this build produced pages a pre-existing partial doesn't
|
||||||
|
// already cover; otherwise keep the older (bigger) partial and just drop the tmp.
|
||||||
|
const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_);
|
||||||
|
|
||||||
|
bool committed = false;
|
||||||
|
if (worthKeeping) {
|
||||||
|
// Capture the parse watermark and commit before tearing the parser down (the anchor
|
||||||
|
// map is read from it). The incomplete trailing page is intentionally not flushed:
|
||||||
|
// only fully laid-out pages are persisted, and the rebuild re-derives the rest.
|
||||||
|
const uint32_t consumed = static_cast<uint32_t>(build_->parser->parseBytesConsumed());
|
||||||
|
committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes);
|
||||||
|
if (committed) {
|
||||||
|
partial_ = true;
|
||||||
|
partialPageCount_ = builtPageCount_;
|
||||||
|
partialBytesConsumed_ = consumed;
|
||||||
|
partialTotalBytes_ = build_->totalBytes;
|
||||||
|
LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build_->parser) build_->parser->abortParse();
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
if (!committed && file) {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
}
|
||||||
|
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
build_.reset();
|
||||||
|
buildComplete_ = false;
|
||||||
|
pageCount = partial_ ? partialPageCount_ : 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::abandonBuild() {
|
||||||
|
if (!build_) return;
|
||||||
|
if (build_->parser) build_->parser->abortParse();
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
if (file) {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
}
|
||||||
|
// A parse error would recur against the same HTML, so drop any partial too -- resuming
|
||||||
|
// from it would just re-enter the failing build every open.
|
||||||
|
if (Storage.exists(filePath.c_str())) {
|
||||||
|
Storage.remove(filePath.c_str());
|
||||||
|
}
|
||||||
|
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
build_.reset();
|
||||||
|
buildComplete_ = false;
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Page> Section::loadPageDuringBuild(const int page) {
|
||||||
|
if (!build_ || page < 0 || page >= static_cast<int>(build_->lut.size()) || !file) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const uint32_t pos = build_->lut[page].fileOffset;
|
||||||
|
if (pos == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// The .bin is open O_RDWR for the build. Read the already-written page, then restore
|
||||||
|
// the write cursor so the next onPageComplete keeps appending where it left off.
|
||||||
|
const uint32_t writePos = file.position();
|
||||||
|
file.seek(pos);
|
||||||
|
auto p = Page::deserialize(file);
|
||||||
|
file.seek(writePos);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a page from the committed file at filePath (finalized section or partial from a
|
||||||
|
// previous session). Uses a local handle so it is safe while a build holds the member
|
||||||
|
// `file` open on the tmp .bin.
|
||||||
|
std::unique_ptr<Page> Section::loadPageAt(const int page) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
|
||||||
uint32_t lutOffset;
|
uint32_t lutOffset;
|
||||||
serialization::readPod(file, lutOffset);
|
serialization::readPod(f, lutOffset);
|
||||||
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
f.seek(lutOffset + sizeof(uint32_t) * page);
|
||||||
uint32_t pagePos;
|
uint32_t pagePos;
|
||||||
serialization::readPod(file, pagePos);
|
serialization::readPod(f, pagePos);
|
||||||
file.seek(pagePos);
|
f.seek(pagePos);
|
||||||
|
|
||||||
auto page = Page::deserialize(file);
|
return Page::deserialize(f);
|
||||||
// Explicit close() required: member variable persists beyond function scope
|
// No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
||||||
file.close();
|
}
|
||||||
return page;
|
|
||||||
|
std::unique_ptr<Page> Section::loadPage(const int page) {
|
||||||
|
if (page < 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (build_ && page < static_cast<int>(build_->lut.size())) {
|
||||||
|
return loadPageDuringBuild(page);
|
||||||
|
}
|
||||||
|
// Not (yet) in the active build: serve from the file on disk -- a finalized section,
|
||||||
|
// or a partial from a previous session whose pages the rebuild hasn't reached again.
|
||||||
|
const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount);
|
||||||
|
if (page >= onDisk) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return loadPageAt(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Section::getTextFromSectionFile() {
|
std::string Section::getTextFromSectionFile() {
|
||||||
std::string fullText;
|
std::string fullText;
|
||||||
auto p = this->loadPageFromSectionFile();
|
auto p = loadPage(currentPage);
|
||||||
if (p) {
|
if (p) {
|
||||||
for (const auto& el : p->elements) {
|
for (const auto& el : p->elements) {
|
||||||
if (el->getTag() == TAG_PageLine) {
|
if (el->getTag() == TAG_PageLine) {
|
||||||
const auto& line = static_cast<const PageLine&>(*el);
|
const auto& line = static_cast<const PageLine&>(*el);
|
||||||
if (line.getBlock()) {
|
if (line.getBlock()) {
|
||||||
const auto& words = line.getBlock()->getWords();
|
const auto& block = *line.getBlock();
|
||||||
for (const auto& w : words) {
|
for (uint16_t i = 0; i < block.wordCount(); i++) {
|
||||||
if (!fullText.empty()) fullText += " ";
|
if (!fullText.empty()) fullText += " ";
|
||||||
fullText += w;
|
fullText += block.wordText(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,6 +765,15 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only a finalized section's count is the chapter total; a partial's count is just the
|
||||||
|
// suspended build's watermark, which would skew progress mapping. Callers fall back to
|
||||||
|
// their own estimates.
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(f, version);
|
||||||
|
if (version != SECTION_FILE_VERSION) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
||||||
uint16_t count;
|
uint16_t count;
|
||||||
serialization::readPod(f, count);
|
serialization::readPod(f, count);
|
||||||
|
|||||||
+120
-7
@@ -3,11 +3,14 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
class Page;
|
class Page;
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
|
class ChapterHtmlSlimParser;
|
||||||
|
class CssParser;
|
||||||
|
|
||||||
class Section {
|
class Section {
|
||||||
std::shared_ptr<Epub> epub;
|
std::shared_ptr<Epub> epub;
|
||||||
@@ -21,16 +24,68 @@ class Section {
|
|||||||
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
||||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
|
// Page-offset table entry, kept in RAM while an incremental build is running so
|
||||||
|
// already-built pages can be located in the partially-written .bin.
|
||||||
|
struct PageLutEntry {
|
||||||
|
uint32_t fileOffset;
|
||||||
|
uint16_t paragraphIndex;
|
||||||
|
uint16_t listItemIndex;
|
||||||
|
};
|
||||||
|
// Held only while an incremental build is in progress (see startBuild). Carries the
|
||||||
|
// live parser plus the strings it references (the parser stores them by reference)
|
||||||
|
// and the in-RAM page-offset table.
|
||||||
|
struct BuildContext {
|
||||||
|
std::unique_ptr<ChapterHtmlSlimParser> parser;
|
||||||
|
std::vector<PageLutEntry> lut;
|
||||||
|
std::string parsePath;
|
||||||
|
std::string contentBase;
|
||||||
|
std::string imageBasePath;
|
||||||
|
std::string htmlPath;
|
||||||
|
std::string tmpHtmlPath;
|
||||||
|
bool reusedHtml = false;
|
||||||
|
CssParser* cssParser = nullptr;
|
||||||
|
// HTML byte progress, for estimating the section's total page count while it's still building.
|
||||||
|
uint32_t bytesConsumed = 0;
|
||||||
|
uint32_t totalBytes = 0;
|
||||||
|
// Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its
|
||||||
|
// last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions;
|
||||||
|
// the EMA is stepped once per build advance (not per redraw) to damp that wobble.
|
||||||
|
float smoothedEstimate = 0;
|
||||||
|
uint32_t smoothedAtConsumed = 0;
|
||||||
|
};
|
||||||
|
std::unique_ptr<BuildContext> build_;
|
||||||
|
bool buildComplete_ = false;
|
||||||
|
// Pages laid out by the active build (== build_->lut.size()). Distinct from pageCount,
|
||||||
|
// which is the pages *available to read* and also counts a loaded partial file's pages.
|
||||||
|
uint16_t builtPageCount_ = 0;
|
||||||
|
// A partial section file (suspended build from a previous session) is loaded at filePath.
|
||||||
|
// Its pages 0..partialPageCount_-1 are readable while a rebuild extends past them.
|
||||||
|
bool partial_ = false;
|
||||||
|
uint16_t partialPageCount_ = 0;
|
||||||
|
// Parse watermark from the partial's trailer, for estimating the total page count.
|
||||||
|
uint32_t partialBytesConsumed_ = 0;
|
||||||
|
uint32_t partialTotalBytes_ = 0;
|
||||||
|
bool finalizeBuild();
|
||||||
|
// Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the
|
||||||
|
// header, stamp the version byte, and swap the tmp .bin over filePath.
|
||||||
|
bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes);
|
||||||
|
// Builds write here and are swapped over filePath only on commit, so a prior
|
||||||
|
// partial/finalized file stays readable while a rebuild is in progress.
|
||||||
|
std::string binTmpPath() const { return filePath + ".part"; }
|
||||||
|
|
||||||
|
std::unique_ptr<Page> loadPageAt(int page) const;
|
||||||
|
// Read a page already laid out by the in-progress build (page < build LUT size), from
|
||||||
|
// the partially-written tmp .bin without disturbing the build's write cursor.
|
||||||
|
std::unique_ptr<Page> loadPageDuringBuild(int page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
uint16_t pageCount = 0;
|
uint16_t pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
// Constructor and destructor are out-of-line: BuildContext holds a unique_ptr to the
|
||||||
: epub(epub),
|
// forward-declared ChapterHtmlSlimParser, whose full definition is only visible in the .cpp.
|
||||||
spineIndex(spineIndex),
|
explicit Section(const std::shared_ptr<Epub>& epub, int spineIndex, GfxRenderer& renderer);
|
||||||
renderer(renderer),
|
~Section();
|
||||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
|
||||||
~Section() = default;
|
|
||||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
uint8_t imageRendering, bool focusReadingEnabled);
|
uint8_t imageRendering, bool focusReadingEnabled);
|
||||||
@@ -39,12 +94,70 @@ class Section {
|
|||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
uint8_t imageRendering, bool focusReadingEnabled,
|
uint8_t imageRendering, bool focusReadingEnabled,
|
||||||
const std::function<void()>& popupFn = nullptr);
|
const std::function<void()>& popupFn = nullptr);
|
||||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
|
||||||
|
// Incremental build: lay out the section a few pages at a time so a large chapter
|
||||||
|
// can show its first page immediately and keep the UI responsive while the rest
|
||||||
|
// builds. createSectionFile() above is the one-shot wrapper over these.
|
||||||
|
// if (!startBuild(...)) fail;
|
||||||
|
// each tick: buildSomeMore(N); render up to pageCount; when isBuildComplete() stop.
|
||||||
|
bool startBuild(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
|
uint8_t imageRendering, bool focusReadingEnabled, const std::function<void()>& popupFn = nullptr);
|
||||||
|
// Lay out up to maxPages more pages (maxPages <= 0 = build to completion). Returns
|
||||||
|
// false on error (the build is abandoned). Sets isBuildComplete() when finished.
|
||||||
|
bool buildSomeMore(int maxPages);
|
||||||
|
bool isBuilding() const { return static_cast<bool>(build_); }
|
||||||
|
bool isBuildComplete() const { return buildComplete_; }
|
||||||
|
// Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based
|
||||||
|
// estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine
|
||||||
|
// is still building, so "page X of Y" / progress don't read off the small build watermark.
|
||||||
|
uint16_t estimatedTotalPages() const;
|
||||||
|
void abandonBuild();
|
||||||
|
// Persist an in-progress build as a partial section file (version sentinel + LUTs +
|
||||||
|
// watermark trailer) instead of discarding it, so the next open of this spine can show
|
||||||
|
// its pages instantly and only rebuild in the background. Called by the destructor, so
|
||||||
|
// any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a
|
||||||
|
// pre-existing partial when it covers more pages than this build reached.
|
||||||
|
void suspendBuild();
|
||||||
|
// True when a partial file was loaded: pageCount is a watermark, not the chapter total.
|
||||||
|
bool isPartial() const { return partial_; }
|
||||||
|
|
||||||
|
// Unified page read: from the active build if it has reached the page, otherwise from
|
||||||
|
// the on-disk file (finalized section, or a partial the rebuild hasn't caught up to).
|
||||||
|
std::unique_ptr<Page> loadPage(int page);
|
||||||
|
|
||||||
std::string getTextFromSectionFile();
|
std::string getTextFromSectionFile();
|
||||||
|
|
||||||
|
// Resolve an anchor from the in-progress build first, then the on-disk anchor map
|
||||||
|
// (covers finalized sections and partials from a previous session).
|
||||||
|
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// MEMFIX-PORT: section resident-bytes audit accessor; portable
|
||||||
|
// Approximate resident heap for the audit log. Steady state (no build) a
|
||||||
|
// Section holds little beyond itself; during a build the page LUT and path
|
||||||
|
// strings dominate (the parser's internal footprint is not walked here).
|
||||||
|
size_t residentBytes() const {
|
||||||
|
size_t total = sizeof(Section) + filePath.capacity();
|
||||||
|
if (build_) {
|
||||||
|
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
|
||||||
|
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
|
||||||
|
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
|
||||||
|
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
|
||||||
|
bool hasHtmlCache() const;
|
||||||
|
|
||||||
// Look up the page number for an anchor id from the section cache file.
|
// Look up the page number for an anchor id from the section cache file.
|
||||||
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// Look up an anchor among the pages built so far by the in-progress build, so an anchor jump
|
||||||
|
// (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the
|
||||||
|
// whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build.
|
||||||
|
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
||||||
|
|
||||||
// Get the page count from the section cache file without fully loading it.
|
// Get the page count from the section cache file without fully loading it.
|
||||||
std::optional<uint16_t> getCachedPageCount() const;
|
std::optional<uint16_t> getCachedPageCount() const;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ struct BlockStyle {
|
|||||||
bool isRtl = false; // true if resolved direction is RTL
|
bool isRtl = false; // true if resolved direction is RTL
|
||||||
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
||||||
|
|
||||||
|
// Set when this block was created by a <br> element. Used by startNewTextBlock to inject
|
||||||
|
// a full line-height gap when the <br> block stays empty (section-break use case).
|
||||||
|
// NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
|
||||||
|
bool fromBrElement = false;
|
||||||
|
|
||||||
// Combined insets (margin + padding)
|
// Combined insets (margin + padding)
|
||||||
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
||||||
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
||||||
@@ -92,6 +97,9 @@ struct BlockStyle {
|
|||||||
result.directionDefined = true;
|
result.directionDefined = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fromBrElement is consumed by startNewTextBlock when an empty <br> block
|
||||||
|
// is merged with the following paragraph; never propagate it further.
|
||||||
|
result.fromBrElement = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "Epub/converters/DirectPixelWriter.h"
|
#include "Epub/converters/DirectPixelWriter.h"
|
||||||
#include "Epub/converters/ImageDecoderFactory.h"
|
#include "Epub/converters/ImageDecoderFactory.h"
|
||||||
|
|
||||||
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
|
|||||||
return imagePath + ".pxc";
|
return imagePath + ".pxc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
|
||||||
|
uint16_t& cachedHeight) {
|
||||||
|
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int widthDiff = abs(cachedWidth - expectedWidth);
|
||||||
|
const int heightDiff = abs(cachedHeight - expectedHeight);
|
||||||
|
if (widthDiff > 1 || heightDiff > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t bytesPerRow = (cachedWidth + 3) / 4;
|
||||||
|
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
|
||||||
|
return cacheFile.size() >= expectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
|
||||||
|
// record so an image that failed renders its placeholder directly for the rest
|
||||||
|
// of the reader session instead of paying another placeholder refresh and
|
||||||
|
// decode. The reader clears this on entry so transient memory/storage failures
|
||||||
|
// are retried.
|
||||||
|
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
|
||||||
|
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
|
||||||
|
size_t failedImageCount = 0;
|
||||||
|
|
||||||
|
uint64_t imagePathHash(const std::string& path) {
|
||||||
|
uint64_t hash = 14695981039346656037ull;
|
||||||
|
for (const char c : path) {
|
||||||
|
hash ^= static_cast<uint8_t>(c);
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool imageFailedThisSession(const std::string& path) {
|
||||||
|
const uint64_t hash = imagePathHash(path);
|
||||||
|
for (size_t i = 0; i < failedImageCount; i++) {
|
||||||
|
if (failedImageHashes[i] == hash) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rememberImageFailure(const std::string& path) {
|
||||||
|
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
|
||||||
|
failedImageHashes[failedImageCount++] = imagePathHash(path);
|
||||||
|
}
|
||||||
|
|
||||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||||
int expectedHeight) {
|
int expectedHeight) {
|
||||||
HalFile cacheFile;
|
HalFile cacheFile;
|
||||||
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint16_t cachedWidth, cachedHeight;
|
uint16_t cachedWidth, cachedHeight;
|
||||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
|
||||||
return false;
|
LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
|
||||||
}
|
|
||||||
|
|
||||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
|
||||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
|
||||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
|
||||||
if (widthDiff > 1 || heightDiff > 1) {
|
|
||||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
|
||||||
expectedHeight);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool ImageBlock::hasValidCache() const {
|
||||||
|
const auto cachePath = getCachePath(imagePath);
|
||||||
|
HalFile cacheFile;
|
||||||
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedWidth, cachedHeight;
|
||||||
|
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
|
||||||
|
|
||||||
|
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
|
||||||
|
|
||||||
|
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
|
||||||
|
renderer.fillRect(x, y, width, height, true);
|
||||||
|
if (width > 2 && height > 2) {
|
||||||
|
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||||
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||||
@@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageFailedThisSession(imagePath)) {
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to render from cache first
|
// Try to render from cache first
|
||||||
std::string cachePath = getCachePath(imagePath);
|
std::string cachePath = getCachePath(imagePath);
|
||||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||||
@@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
HalFile file;
|
HalFile file;
|
||||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
size_t fileSize = file.size();
|
size_t fileSize = file.size();
|
||||||
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
|
|
||||||
if (fileSize == 0) {
|
if (fileSize == 0) {
|
||||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||||
if (!decoder) {
|
if (!decoder) {
|
||||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
|
|||||||
int16_t getHeight() const { return height; }
|
int16_t getHeight() const { return height; }
|
||||||
|
|
||||||
bool imageExists() const;
|
bool imageExists() const;
|
||||||
|
bool hasValidCache() const;
|
||||||
|
bool needsDecode() const;
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
|
||||||
|
static void clearSessionRenderFailures();
|
||||||
|
|
||||||
BlockType getType() override { return IMAGE_BLOCK; }
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
bool isEmpty() override { return false; }
|
bool isEmpty() override { return false; }
|
||||||
|
|||||||
@@ -3,30 +3,159 @@
|
|||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
size_t TextBlock::arenaSize(const uint16_t wordCount, const bool hasFocus, const uint16_t textBytes) {
|
||||||
|
// Layout documented in TextBlock.h: 16-bit arrays first, then 8-bit arrays, then text.
|
||||||
|
size_t size = static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(int16_t) + sizeof(uint8_t));
|
||||||
|
if (hasFocus) {
|
||||||
|
size += static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
return size + textBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::bindArenaPointers() {
|
||||||
|
uint8_t* base = arena.get();
|
||||||
|
const size_t wc = numWords;
|
||||||
|
textOffArr = reinterpret_cast<const uint16_t*>(base);
|
||||||
|
xposArr = reinterpret_cast<const int16_t*>(base + wc * 2);
|
||||||
|
size_t off = wc * 4;
|
||||||
|
if (focusPresent) {
|
||||||
|
focusSuffixXArr = reinterpret_cast<const uint16_t*>(base + off);
|
||||||
|
off += wc * 2;
|
||||||
|
}
|
||||||
|
stylesArr = base + off;
|
||||||
|
off += wc;
|
||||||
|
if (focusPresent) {
|
||||||
|
focusBoundaryArr = base + off;
|
||||||
|
off += wc;
|
||||||
|
}
|
||||||
|
textArr = reinterpret_cast<const char*>(base + off);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextBlock::TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||||
|
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||||
|
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle)
|
||||||
|
: blockStyle(blockStyle) {
|
||||||
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
||||||
// When present, they must be sized in lockstep with words[].
|
// When present, they must be sized in lockstep with words[].
|
||||||
const bool hasFocus = !wordFocusBoundary.empty();
|
const bool hasFocus = !focusBoundary.empty();
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() || words.size() > 10000 ||
|
||||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
(hasFocus && (words.size() != focusBoundary.size() || words.size() != focusSuffixX.size()))) {
|
||||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
LOG_ERR("TXB", "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)",
|
||||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size(),
|
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||||
(uint32_t)wordFocusBoundary.size(), (uint32_t)wordFocusSuffixX.size());
|
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(focusBoundary.size()),
|
||||||
|
static_cast<uint32_t>(focusSuffixX.size()));
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
numWords = static_cast<uint16_t>(words.size());
|
||||||
|
focusPresent = hasFocus;
|
||||||
|
if (numWords == 0) {
|
||||||
|
return; // valid empty block, no arena
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1: total text size, one NUL per word. A line is at most a physical
|
||||||
|
// row of the page, so uint16_t offsets are ample; reject anything larger.
|
||||||
|
size_t totalText = 0;
|
||||||
|
for (const auto& w : words) totalText += w.size() + 1;
|
||||||
|
if (totalText > UINT16_MAX) {
|
||||||
|
LOG_ERR("TXB", "Construction failed: text size %u exceeds arena limit", static_cast<uint32_t>(totalText));
|
||||||
|
numWords = 0;
|
||||||
|
focusPresent = false;
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textBytes = static_cast<uint16_t>(totalText);
|
||||||
|
|
||||||
|
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||||
|
arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||||
|
if (!arena) {
|
||||||
|
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||||
|
numWords = 0;
|
||||||
|
textBytes = 0;
|
||||||
|
focusPresent = false;
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bindArenaPointers();
|
||||||
|
|
||||||
|
// Pass 2: fill. Mutable aliases of the const views bound above.
|
||||||
|
auto* textOff = const_cast<uint16_t*>(textOffArr);
|
||||||
|
auto* xpos = const_cast<int16_t*>(xposArr);
|
||||||
|
auto* styles = const_cast<uint8_t*>(stylesArr);
|
||||||
|
auto* text = const_cast<char*>(textArr);
|
||||||
|
uint16_t off = 0;
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
textOff[i] = off;
|
||||||
|
xpos[i] = wordXpos[i];
|
||||||
|
styles[i] = static_cast<uint8_t>(wordStyles[i]);
|
||||||
|
memcpy(text + off, words[i].data(), words[i].size());
|
||||||
|
off += static_cast<uint16_t>(words[i].size());
|
||||||
|
text[off++] = '\0';
|
||||||
|
}
|
||||||
|
if (focusPresent) {
|
||||||
|
auto* suffixX = const_cast<uint16_t*>(focusSuffixXArr);
|
||||||
|
auto* boundary = const_cast<uint8_t*>(focusBoundaryArr);
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
suffixX[i] = focusSuffixX[i];
|
||||||
|
boundary[i] = focusBoundary[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
|
if (!isValid) {
|
||||||
|
LOG_ERR("TXB", "Render skipped: invalid block");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool scanning = renderer.isFontCacheScanning();
|
const bool scanning = renderer.isFontCacheScanning();
|
||||||
const int ascender = renderer.getFontAscenderSize(fontId);
|
const int ascender = renderer.getFontAscenderSize(fontId);
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
|
||||||
const int wordX = wordXpos[i] + x;
|
struct DecorationLineTracker {
|
||||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
EpdFontFamily::Style style;
|
||||||
const auto baseDir = static_cast<BidiUtils::BidiBaseDir>(
|
int yOffset;
|
||||||
BidiUtils::detectParagraphLevel(words[i].c_str(), blockStyle.isRtl ? 1 : 0));
|
int startX = -1;
|
||||||
const uint8_t boundary = hasFocus ? wordFocusBoundary[i] : 0;
|
int endX = -1;
|
||||||
|
int yPos = 0;
|
||||||
|
|
||||||
|
bool active() const { return startX != -1; }
|
||||||
|
void reset() {
|
||||||
|
startX = -1;
|
||||||
|
endX = -1;
|
||||||
|
yPos = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DecorationLineTracker decorationLines[] = {
|
||||||
|
{EpdFontFamily::UNDERLINE, ascender + 2},
|
||||||
|
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto flushDecoration = [&](DecorationLineTracker& line) {
|
||||||
|
if (line.active()) {
|
||||||
|
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
|
||||||
|
line.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto flushDecorations = [&]() {
|
||||||
|
for (auto& line : decorationLines) {
|
||||||
|
flushDecoration(line);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
const char* word = wordText(i);
|
||||||
|
const int wordX = xposArr[i] + x;
|
||||||
|
const EpdFontFamily::Style currentStyle = wordStyle(i);
|
||||||
|
const auto baseDir =
|
||||||
|
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
|
||||||
|
const uint8_t boundary = focusBoundary(i);
|
||||||
|
|
||||||
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
||||||
// drawText, so these offsets are chosen relative to the full-size ascender:
|
// drawText, so these offsets are chosen relative to the full-size ascender:
|
||||||
@@ -49,54 +178,81 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
||||||
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
||||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
||||||
const size_t boldLen = std::min<size_t>({static_cast<size_t>(boundary), words[i].size(), sizeof(boldBuf) - 1});
|
const size_t boldLen =
|
||||||
memcpy(boldBuf, words[i].c_str(), boldLen);
|
std::min<size_t>({static_cast<size_t>(boundary), static_cast<size_t>(wordTextLen(i)), sizeof(boldBuf) - 1});
|
||||||
|
memcpy(boldBuf, word, boldLen);
|
||||||
boldBuf[boldLen] = '\0';
|
boldBuf[boldLen] = '\0';
|
||||||
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
||||||
const int suffixX = wordX + wordFocusSuffixX[i];
|
const int suffixX = wordX + focusSuffixXArr[i];
|
||||||
renderer.drawText(fontId, suffixX, wordY, words[i].c_str() + boldLen, true, currentStyle, baseDir);
|
renderer.drawText(fontId, suffixX, wordY, word + boldLen, true, currentStyle, baseDir);
|
||||||
} else {
|
} else {
|
||||||
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir);
|
renderer.drawText(fontId, wordX, wordY, word, true, currentStyle, baseDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
if (scanning) {
|
||||||
const std::string& w = words[i];
|
continue;
|
||||||
int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
|
}
|
||||||
const int underlineY = wordY + ascender + 2;
|
|
||||||
|
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
|
||||||
|
int lineStartX = wordX;
|
||||||
|
int lineWidth = renderer.getTextWidth(fontId, word, currentStyle, baseDir);
|
||||||
|
|
||||||
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||||
underlineWidth = (underlineWidth + 1) / 2;
|
lineWidth = (lineWidth + 1) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true);
|
// Do not decorate the synthetic em-space used for paragraph indentation.
|
||||||
|
if (wordTextLen(i) >= 3 && static_cast<uint8_t>(word[0]) == 0xE2 && static_cast<uint8_t>(word[1]) == 0x80 &&
|
||||||
|
static_cast<uint8_t>(word[2]) == 0x83) {
|
||||||
|
const char* visibleText = word + 3;
|
||||||
|
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||||
|
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
|
||||||
|
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||||
|
lineWidth = (lineWidth + 1) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& line : decorationLines) {
|
||||||
|
if ((currentStyle & line.style) == 0) {
|
||||||
|
flushDecoration(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lineY = wordY + line.yOffset;
|
||||||
|
if (line.active() && line.yPos != lineY) {
|
||||||
|
flushDecoration(line);
|
||||||
|
}
|
||||||
|
if (!line.active()) {
|
||||||
|
line.startX = lineStartX;
|
||||||
|
line.yPos = lineY;
|
||||||
|
}
|
||||||
|
line.endX = lineStartX + lineWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flushDecorations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushDecorations();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextBlock::serialize(HalFile& file) const {
|
bool TextBlock::serialize(HalFile& file) const {
|
||||||
// Focus annotations are optional; vectors are either empty (no splits in this block)
|
if (!isValid) {
|
||||||
// or sized in lockstep with words[].
|
LOG_ERR("TXB", "Serialization failed: invalid block");
|
||||||
const bool hasFocus = !wordFocusBoundary.empty();
|
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
|
||||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
|
||||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
|
||||||
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
|
||||||
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(wordFocusBoundary.size()),
|
|
||||||
static_cast<uint32_t>(wordFocusSuffixX.size()));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word data
|
// Word data: scalars, then the arena verbatim -- its in-memory layout is
|
||||||
serialization::writePod(file, static_cast<uint16_t>(words.size()));
|
// exactly the on-disk layout (see TextBlock.h), so one write covers all
|
||||||
for (const auto& w : words) serialization::writeString(file, w);
|
// per-word arrays and the text blob.
|
||||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
serialization::writePod(file, numWords);
|
||||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
serialization::writePod(file, static_cast<uint8_t>(focusPresent ? 1 : 0));
|
||||||
// Focus block: 1-byte presence flag, followed by per-word vectors only when present.
|
serialization::writePod(file, textBytes);
|
||||||
// Saves 3 bytes/word when focus reading is disabled or no word on this line was split.
|
if (numWords > 0) {
|
||||||
serialization::writePod(file, static_cast<uint8_t>(hasFocus ? 1 : 0));
|
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||||
if (hasFocus) {
|
if (file.write(arena.get(), size) != size) {
|
||||||
for (auto b : wordFocusBoundary) serialization::writePod(file, b);
|
LOG_ERR("TXB", "Serialization failed: arena write (%u bytes)", static_cast<uint32_t>(size));
|
||||||
for (auto sx : wordFocusSuffixX) serialization::writePod(file, sx);
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style (alignment + margins/padding/indent)
|
// Style (alignment + margins/padding/indent)
|
||||||
@@ -120,41 +276,64 @@ bool TextBlock::serialize(HalFile& file) const {
|
|||||||
|
|
||||||
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
||||||
uint16_t wc;
|
uint16_t wc;
|
||||||
std::vector<std::string> words;
|
uint8_t hasFocus;
|
||||||
std::vector<int16_t> wordXpos;
|
uint16_t textBytes;
|
||||||
std::vector<EpdFontFamily::Style> wordStyles;
|
|
||||||
std::vector<uint8_t> wordFocusBoundary;
|
|
||||||
std::vector<uint16_t> wordFocusSuffixX;
|
|
||||||
BlockStyle blockStyle;
|
|
||||||
|
|
||||||
// Word count
|
|
||||||
serialization::readPod(file, wc);
|
serialization::readPod(file, wc);
|
||||||
|
serialization::readPod(file, hasFocus);
|
||||||
|
serialization::readPod(file, textBytes);
|
||||||
|
|
||||||
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
// Sanity checks: cap the arena allocation and reject impossible geometry
|
||||||
|
// (every word carries at least its NUL terminator).
|
||||||
if (wc > 10000) {
|
if (wc > 10000) {
|
||||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
if ((wc == 0 && textBytes != 0) || (wc > 0 && textBytes < wc)) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: bad text size %u for %u words", textBytes, wc);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Word data
|
std::unique_ptr<TextBlock> block(new (std::nothrow) TextBlock());
|
||||||
words.resize(wc);
|
if (!block) {
|
||||||
wordXpos.resize(wc);
|
LOG_ERR("TXB", "OOM: TextBlock");
|
||||||
wordStyles.resize(wc);
|
return nullptr;
|
||||||
for (auto& w : words) serialization::readString(file, w);
|
}
|
||||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
block->numWords = wc;
|
||||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
block->textBytes = textBytes;
|
||||||
// Focus block: presence flag, then vectors only if present. Empty vectors when absent
|
block->focusPresent = hasFocus != 0;
|
||||||
// signal "no splits in this block" to render() (zero per-word RAM cost).
|
|
||||||
uint8_t hasFocus;
|
if (wc > 0) {
|
||||||
serialization::readPod(file, hasFocus);
|
const size_t size = arenaSize(wc, block->focusPresent, textBytes);
|
||||||
if (hasFocus) {
|
block->arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||||
wordFocusBoundary.resize(wc);
|
if (!block->arena) {
|
||||||
wordFocusSuffixX.resize(wc);
|
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||||
for (auto& b : wordFocusBoundary) serialization::readPod(file, b);
|
return nullptr;
|
||||||
for (auto& sx : wordFocusSuffixX) serialization::readPod(file, sx);
|
}
|
||||||
|
if (file.read(block->arena.get(), size) != size) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: arena read (%u bytes)", static_cast<uint32_t>(size));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
block->bindArenaPointers();
|
||||||
|
|
||||||
|
// Validate offsets before anything dereferences wordText(): offset 0 first,
|
||||||
|
// strictly increasing, in bounds, and every word NUL-terminated (word i ends
|
||||||
|
// at the byte before offset i+1; the last word at the last text byte).
|
||||||
|
const uint16_t* textOff = block->textOffArr;
|
||||||
|
const char* text = block->textArr;
|
||||||
|
if (textOff[0] != 0 || text[textBytes - 1] != '\0') {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: corrupt text layout");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (uint16_t i = 1; i < wc; i++) {
|
||||||
|
if (textOff[i] <= textOff[i - 1] || textOff[i] >= textBytes || text[textOff[i] - 1] != '\0') {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: corrupt word offset %u", i);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style (alignment + margins/padding/indent)
|
// Style (alignment + margins/padding/indent)
|
||||||
|
BlockStyle& blockStyle = block->blockStyle;
|
||||||
serialization::readPod(file, blockStyle.alignment);
|
serialization::readPod(file, blockStyle.alignment);
|
||||||
serialization::readPod(file, blockStyle.textAlignDefined);
|
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||||
serialization::readPod(file, blockStyle.marginTop);
|
serialization::readPod(file, blockStyle.marginTop);
|
||||||
@@ -170,7 +349,5 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
|||||||
serialization::readPod(file, blockStyle.isRtl);
|
serialization::readPod(file, blockStyle.isRtl);
|
||||||
serialization::readPod(file, blockStyle.directionDefined);
|
serialization::readPod(file, blockStyle.directionDefined);
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
|
return block;
|
||||||
std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
|
|
||||||
blockStyle));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,42 +9,83 @@
|
|||||||
#include "Block.h"
|
#include "Block.h"
|
||||||
#include "BlockStyle.h"
|
#include "BlockStyle.h"
|
||||||
|
|
||||||
// Represents a line of text on a page
|
// Represents a line of text on a page.
|
||||||
|
//
|
||||||
|
// All per-word data lives in ONE flat heap allocation (the arena) instead of
|
||||||
|
// six parallel vectors: a resident page holds ~25-30 of these blocks, and the
|
||||||
|
// vector-of-string layout cost ~250 throwing allocations per page load, which
|
||||||
|
// was the primary driver of heap fragmentation on the ESP32-C3.
|
||||||
|
//
|
||||||
|
// Arena layout, in order (2-byte alignment holds by construction: all 16-bit
|
||||||
|
// arrays come first and the arena base is allocator-aligned; RISC-V faults on
|
||||||
|
// unaligned multi-byte access):
|
||||||
|
// uint16_t textOff[wordCount] byte offset of word i's text in text[]
|
||||||
|
// int16_t xpos[wordCount]
|
||||||
|
// uint16_t focusSuffixX[wordCount] present only when focusPresent
|
||||||
|
// uint8_t styles[wordCount]
|
||||||
|
// uint8_t focusBoundary[wordCount] present only when focusPresent
|
||||||
|
// char text[textBytes] all words back to back, NUL-terminated
|
||||||
|
//
|
||||||
|
// Each word is stored NUL-terminated so render() can hand `text + textOff[i]`
|
||||||
|
// straight to C APIs (drawText) with no std::string materialization.
|
||||||
|
//
|
||||||
|
// Focus split semantics (unchanged from the vector layout): boundary N > 0
|
||||||
|
// means the first N bytes of word i render bold, the remainder in the base
|
||||||
|
// style. N is bounded to 9 codepoints (<= 36 UTF-8 bytes) by the clamp in
|
||||||
|
// ParsedText::addWord. focusSuffixX is the pre-computed pixel offset from the
|
||||||
|
// word start to the regular suffix. Both arrays are omitted from the arena
|
||||||
|
// entirely when no word on the line has a split (zero per-word RAM cost when
|
||||||
|
// focus reading is disabled).
|
||||||
class TextBlock final : public Block {
|
class TextBlock final : public Block {
|
||||||
private:
|
private:
|
||||||
std::vector<std::string> words;
|
|
||||||
std::vector<int16_t> wordXpos;
|
|
||||||
std::vector<EpdFontFamily::Style> wordStyles;
|
|
||||||
// Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
|
|
||||||
// the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
|
|
||||||
// N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
|
|
||||||
// FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
|
|
||||||
// Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
|
|
||||||
// when focus reading is disabled, or on lines that happen to contain no splittable words).
|
|
||||||
std::vector<uint8_t> wordFocusBoundary;
|
|
||||||
// Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
|
|
||||||
// Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
|
|
||||||
// Empty in lockstep with wordFocusBoundary.
|
|
||||||
std::vector<uint16_t> wordFocusSuffixX;
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
|
uint16_t numWords = 0;
|
||||||
|
uint16_t textBytes = 0; // total size of the text region, including NULs
|
||||||
|
bool focusPresent = false;
|
||||||
|
bool isValid = true;
|
||||||
|
// The ONLY allocation: makeUniqueNoThrow, so OOM yields an invalid block
|
||||||
|
// instead of abort() (bare new is not nothrow with -fno-exceptions).
|
||||||
|
std::unique_ptr<uint8_t[]> arena;
|
||||||
|
// Typed views into the arena, bound once after the arena is filled. All
|
||||||
|
// 16-bit bases sit at even offsets, so direct dereference is alignment-safe.
|
||||||
|
const uint16_t* textOffArr = nullptr;
|
||||||
|
const int16_t* xposArr = nullptr;
|
||||||
|
const uint16_t* focusSuffixXArr = nullptr; // null when !focusPresent
|
||||||
|
const uint8_t* stylesArr = nullptr;
|
||||||
|
const uint8_t* focusBoundaryArr = nullptr; // null when !focusPresent
|
||||||
|
const char* textArr = nullptr;
|
||||||
|
|
||||||
|
TextBlock() = default; // deserialize() fills the fields directly
|
||||||
|
static size_t arenaSize(uint16_t wordCount, bool hasFocus, uint16_t textBytes);
|
||||||
|
void bindArenaPointers();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
|
// Flatten-on-construct: copies the layout-time vectors into the arena; the
|
||||||
std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
|
// vectors die with the caller. On arena OOM the block is empty and valid()
|
||||||
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
|
// is false -- callers must check and fail the line instead of using it.
|
||||||
: words(std::move(words)),
|
explicit TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||||
wordXpos(std::move(word_xpos)),
|
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||||
wordStyles(std::move(word_styles)),
|
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle = BlockStyle());
|
||||||
wordFocusBoundary(std::move(focus_boundary)),
|
|
||||||
wordFocusSuffixX(std::move(focus_suffix_x)),
|
|
||||||
blockStyle(blockStyle) {}
|
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
|
TextBlock(const TextBlock&) = delete;
|
||||||
|
TextBlock& operator=(const TextBlock&) = delete;
|
||||||
|
|
||||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||||
const std::vector<std::string>& getWords() const { return words; }
|
bool isEmpty() override { return numWords == 0; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool valid() const { return isValid; }
|
||||||
size_t wordCount() const { return words.size(); }
|
uint16_t wordCount() const { return numWords; }
|
||||||
// given a renderer works out where to break the words into lines
|
// NUL-terminated by construction; safe to pass to C APIs directly.
|
||||||
|
const char* wordText(const uint16_t i) const { return textArr + textOffArr[i]; }
|
||||||
|
uint16_t wordTextLen(const uint16_t i) const {
|
||||||
|
const uint16_t end = (i + 1 < numWords) ? textOffArr[i + 1] : textBytes;
|
||||||
|
return end - textOffArr[i] - 1; // exclude the NUL
|
||||||
|
}
|
||||||
|
int16_t wordXpos(const uint16_t i) const { return xposArr[i]; }
|
||||||
|
EpdFontFamily::Style wordStyle(const uint16_t i) const { return static_cast<EpdFontFamily::Style>(stylesArr[i]); }
|
||||||
|
uint8_t focusBoundary(const uint16_t i) const { return focusPresent ? focusBoundaryArr[i] : 0; }
|
||||||
|
uint16_t focusSuffixX(const uint16_t i) const { return focusPresent ? focusSuffixXArr[i] : 0; }
|
||||||
|
|
||||||
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; }
|
||||||
bool serialize(HalFile& file) const;
|
bool serialize(HalFile& file) const;
|
||||||
|
|||||||
@@ -99,24 +99,58 @@ int bytesPerPixelFromType(int pixelType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
|
int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; }
|
||||||
|
|
||||||
|
int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) {
|
||||||
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||||
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||||
|
if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) {
|
||||||
|
pitch = packedRowBytes(srcWidth, bitsPerSample);
|
||||||
|
}
|
||||||
return ((pitch + 1) * 2) + 32;
|
return ((pitch + 1) * 2) + 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isSupportedBitDepth(int pixelType, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return true;
|
||||||
|
if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false;
|
||||||
|
return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return pixels[x];
|
||||||
|
|
||||||
|
const int bitOffset = x * bitsPerSample;
|
||||||
|
const int shift = 8 - bitsPerSample - (bitOffset & 7);
|
||||||
|
const uint8_t mask = (1U << bitsPerSample) - 1;
|
||||||
|
return (pixels[bitOffset >> 3] >> shift) & mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return sample;
|
||||||
|
const uint8_t maxSample = (1U << bitsPerSample) - 1;
|
||||||
|
return static_cast<uint8_t>((sample * 255U) / maxSample);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert entire source line to grayscale with alpha blending to white background.
|
// Convert entire source line to grayscale with alpha blending to white background.
|
||||||
|
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
|
||||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||||
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
|
||||||
|
uint8_t* palette, int hasAlpha) {
|
||||||
switch (pixelType) {
|
switch (pixelType) {
|
||||||
case PNG_PIXEL_GRAYSCALE:
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
memcpy(grayLine, pPixels, width);
|
if (bitsPerSample == 8) {
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PNG_PIXEL_TRUECOLOR:
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t* p = &pPixels[x * 3];
|
const uint8_t* p = &pPixels[x * 3];
|
||||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -125,7 +159,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
|||||||
if (palette) {
|
if (palette) {
|
||||||
if (hasAlpha) {
|
if (hasAlpha) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t idx = pPixels[x];
|
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||||
uint8_t* p = &palette[idx * 3];
|
uint8_t* p = &palette[idx * 3];
|
||||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
uint8_t alpha = palette[768 + idx];
|
uint8_t alpha = palette[768 + idx];
|
||||||
@@ -133,12 +167,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t* p = &palette[pPixels[x] * 3];
|
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||||
|
uint8_t* p = &palette[idx * 3];
|
||||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
memcpy(grayLine, pPixels, width);
|
for (int x = 0; x < width; x++) {
|
||||||
|
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -152,7 +189,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
|||||||
|
|
||||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t* p = &pPixels[x * 4];
|
const uint8_t* p = &pPixels[x * 4];
|
||||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
uint8_t alpha = p[3];
|
uint8_t alpha = p[3];
|
||||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
@@ -172,74 +209,83 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
|||||||
int srcY = pDraw->y;
|
int srcY = pDraw->y;
|
||||||
int srcWidth = ctx->srcWidth;
|
int srcWidth = ctx->srcWidth;
|
||||||
|
|
||||||
// Calculate destination Y with scaling
|
// Map source rows with the exact output-height ratio. During downscaling,
|
||||||
int dstY = (int)(srcY * ctx->scale);
|
// multiple source rows can select the same output row; during upscaling, one
|
||||||
|
// source row must be repeated across every output row in its range. Emitting
|
||||||
|
// only the first row of an upscale leaves zero-filled (black) gaps in the
|
||||||
|
// streamed pixel cache.
|
||||||
|
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
|
||||||
|
int endDstY = firstDstY + 1;
|
||||||
|
if (ctx->dstHeight > ctx->srcHeight) {
|
||||||
|
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||||
if (dstY == ctx->lastDstY) return 1;
|
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||||
ctx->lastDstY = dstY;
|
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||||
|
|
||||||
// Check bounds
|
|
||||||
if (dstY >= ctx->dstHeight) return 1;
|
|
||||||
|
|
||||||
int outY = ctx->config->y + dstY;
|
|
||||||
if (outY >= ctx->screenHeight) return 1;
|
|
||||||
|
|
||||||
// Convert entire source line to grayscale (improves cache locality)
|
// Convert entire source line to grayscale (improves cache locality)
|
||||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
|
||||||
pDraw->iHasAlpha);
|
pDraw->iHasAlpha);
|
||||||
|
|
||||||
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
// Render scaled rows using Bresenham-style integer stepping (no floating-point division)
|
||||||
int dstWidth = ctx->dstWidth;
|
int dstWidth = ctx->dstWidth;
|
||||||
int outXBase = ctx->config->x;
|
int outXBase = ctx->config->x;
|
||||||
int screenWidth = ctx->screenWidth;
|
int screenWidth = ctx->screenWidth;
|
||||||
bool useDithering = ctx->config->useDithering;
|
bool useDithering = ctx->config->useDithering;
|
||||||
bool caching = ctx->caching;
|
|
||||||
|
|
||||||
// Pre-compute orientation and render-mode state once per row
|
// Pre-compute orientation and render-mode state once per callback.
|
||||||
DirectPixelWriter pw;
|
DirectPixelWriter pw;
|
||||||
pw.init(*ctx->renderer);
|
pw.init(*ctx->renderer);
|
||||||
pw.beginRow(outY);
|
|
||||||
|
|
||||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
for (int dstY = firstDstY; dstY < endDstY; dstY++) {
|
||||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
ctx->lastDstY = dstY;
|
||||||
// A flush failure stops caching for the rest of the decode so we never write
|
int outY = ctx->config->y + dstY;
|
||||||
// past the band buffer; finalize() then drops the partial file.
|
if (outY >= ctx->screenHeight) continue;
|
||||||
DirectCacheWriter cw;
|
|
||||||
if (caching) {
|
|
||||||
if (!ctx->cache.advanceTo(dstY)) {
|
|
||||||
caching = false;
|
|
||||||
ctx->caching = false;
|
|
||||||
} else {
|
|
||||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
|
||||||
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int srcX = 0;
|
pw.beginRow(outY);
|
||||||
int error = 0;
|
|
||||||
|
|
||||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||||
int outX = outXBase + dstX;
|
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||||
if (outX < screenWidth) {
|
// A flush failure stops caching for the rest of the decode so we never write
|
||||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
// past the band buffer; finalize() then drops the partial file.
|
||||||
|
bool caching = ctx->caching;
|
||||||
uint8_t ditheredGray;
|
DirectCacheWriter cw;
|
||||||
if (useDithering) {
|
if (caching) {
|
||||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
if (!ctx->cache.advanceTo(dstY)) {
|
||||||
|
caching = false;
|
||||||
|
ctx->caching = false;
|
||||||
} else {
|
} else {
|
||||||
ditheredGray = gray / 85;
|
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||||
if (ditheredGray > 3) ditheredGray = 3;
|
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||||
}
|
}
|
||||||
pw.writePixel(outX, ditheredGray);
|
|
||||||
if (caching) cw.writePixel(outX, ditheredGray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
int srcX = 0;
|
||||||
error += srcWidth;
|
int error = 0;
|
||||||
while (error >= dstWidth) {
|
|
||||||
error -= dstWidth;
|
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||||
srcX++;
|
int outX = outXBase + dstX;
|
||||||
|
if (outX < screenWidth) {
|
||||||
|
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||||
|
|
||||||
|
uint8_t ditheredGray;
|
||||||
|
if (useDithering) {
|
||||||
|
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
ditheredGray = gray / 85;
|
||||||
|
if (ditheredGray > 3) ditheredGray = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, ditheredGray);
|
||||||
|
if (caching) cw.writePixel(outX, ditheredGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||||
|
error += srcWidth;
|
||||||
|
while (error >= dstWidth) {
|
||||||
|
error -= dstWidth;
|
||||||
|
srcX++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,30 +378,44 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
|||||||
}
|
}
|
||||||
ctx.lastDstY = -1; // Reset row tracking
|
ctx.lastDstY = -1; // Reset row tracking
|
||||||
|
|
||||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
|
||||||
ctx.scale, png->getBpp());
|
|
||||||
|
|
||||||
const int pixelType = png->getPixelType();
|
const int pixelType = png->getPixelType();
|
||||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
|
const int bitsPerSample = png->getBpp();
|
||||||
|
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
|
||||||
|
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||||
|
|
||||||
|
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
|
||||||
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||||
LOG_ERR("PNG",
|
LOG_ERR(
|
||||||
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
"PNG",
|
||||||
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
"PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||||
|
requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS);
|
||||||
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (png->getBpp() != 8) {
|
if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
|
||||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
warnUnsupportedFeature(
|
||||||
|
"bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
// The converter expands each source row to 8-bit grayscale before dithering,
|
||||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
// so this scratch buffer is sized by source pixels even when PNGdec reads a
|
||||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
// packed 1/2/4-bit row internally.
|
||||||
if (!ctx.grayLineBuffer) {
|
constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||||
|
const size_t grayBufSize = static_cast<size_t>(ctx.srcWidth);
|
||||||
|
if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) {
|
||||||
|
LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast<unsigned>(grayBufSize),
|
||||||
|
ctx.srcWidth, static_cast<unsigned>(MAX_GRAY_LINE_BUFFER_BYTES));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
|
||||||
|
if (!grayLineBuffer) {
|
||||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||||
|
|
||||||
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||||
// bottom and we emit at most one (downscaled) output row per callback, so the
|
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||||
@@ -375,7 +435,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
|||||||
rc = png->decode(&ctx, 0);
|
rc = png->decode(&ctx, 0);
|
||||||
unsigned long decodeTime = millis() - decodeStart;
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
free(ctx.grayLineBuffer);
|
|
||||||
ctx.grayLineBuffer = nullptr;
|
ctx.grayLineBuffer = nullptr;
|
||||||
|
|
||||||
if (rc != PNG_SUCCESS) {
|
if (rc != PNG_SUCCESS) {
|
||||||
|
|||||||
@@ -67,13 +67,6 @@ constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKe
|
|||||||
[](char a, char b) { return asciiToLower(a) == b; });
|
[](char a, char b) { return asciiToLower(a) == b; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case-insensitive ASCII substring search. Only needed by text-decoration,
|
|
||||||
// which accepts multi-value strings like "underline solid red".
|
|
||||||
constexpr bool icontainsAscii(std::string_view value, std::string_view lowercaseKeyword) {
|
|
||||||
return std::search(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
|
|
||||||
[](char a, char b) { return asciiToLower(a) == b; }) != value.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk s and invoke fn(token) for each non-empty run between delimiters.
|
// Walk s and invoke fn(token) for each non-empty run between delimiters.
|
||||||
// Tokens are boundary-trimmed and yielded as string_views into s; no
|
// Tokens are boundary-trimmed and yielded as string_views into s; no
|
||||||
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
|
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
|
||||||
@@ -252,11 +245,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
|
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
|
||||||
// text-decoration can have multiple space-separated values
|
// text-decoration can have multiple space-separated values. Compare whole tokens
|
||||||
if (icontainsAscii(val, "underline")) {
|
// so malformed values like "notunderline" do not accidentally enable a line.
|
||||||
return CssTextDecoration::Underline;
|
CssTextDecoration result = CssTextDecoration::None;
|
||||||
}
|
bool explicitNone = false;
|
||||||
return CssTextDecoration::None;
|
forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
|
||||||
|
if (iequalsAscii(token, "none")) {
|
||||||
|
explicitNone = true;
|
||||||
|
} else if (iequalsAscii(token, "underline")) {
|
||||||
|
result = result | CssTextDecoration::Underline;
|
||||||
|
} else if (iequalsAscii(token, "line-through")) {
|
||||||
|
result = result | CssTextDecoration::LineThrough;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return explicitNone ? CssTextDecoration::None : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
CssLength CssParser::interpretLength(std::string_view val) {
|
CssLength CssParser::interpretLength(std::string_view val) {
|
||||||
@@ -801,6 +803,9 @@ bool CssParser::loadFromCache() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Size the bucket array up front to avoid incremental rehashes while loading rules.
|
||||||
|
rulesBySelector_.reserve(ruleCount);
|
||||||
|
|
||||||
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
||||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||||
};
|
};
|
||||||
@@ -868,7 +873,7 @@ bool CssParser::loadFromCache() {
|
|||||||
rulesBySelector_.clear();
|
rulesBySelector_.clear();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
style.textDecoration = static_cast<CssTextDecoration>(enumVal & CSS_TEXT_DECORATION_MASK);
|
||||||
|
|
||||||
if (file.read(&enumVal, 1) != 1) {
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
rulesBySelector_.clear();
|
rulesBySelector_.clear();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
class CssParser {
|
class CssParser {
|
||||||
public:
|
public:
|
||||||
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
||||||
static constexpr uint8_t CSS_CACHE_VERSION = 6;
|
static constexpr uint8_t CSS_CACHE_VERSION = 7;
|
||||||
|
|
||||||
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||||
~CssParser() = default;
|
~CssParser() = default;
|
||||||
@@ -67,6 +67,20 @@ class CssParser {
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
||||||
|
|
||||||
|
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
|
||||||
|
// Approximate resident heap of the parsed stylesheet, for the audit log.
|
||||||
|
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
|
||||||
|
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
|
||||||
|
size_t residentBytes() const {
|
||||||
|
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
|
||||||
|
for (const auto& kv : rulesBySelector_) {
|
||||||
|
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
|
||||||
|
total += sizeof(kv);
|
||||||
|
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if any rules have been loaded
|
* Check if any rules have been loaded
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -52,8 +52,19 @@ enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
|||||||
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
||||||
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||||
|
|
||||||
// Text decoration options
|
// Text decoration options. Values are bit flags so CSS can combine multiple line decorations.
|
||||||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
|
||||||
|
|
||||||
|
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
|
||||||
|
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
|
||||||
|
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
|
||||||
|
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
|
||||||
|
|
||||||
// Display options - only None and Block are relevant for e-ink rendering
|
// Display options - only None and Block are relevant for e-ink rendering
|
||||||
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
||||||
|
|||||||
@@ -13,54 +13,57 @@ struct EntityPair {
|
|||||||
|
|
||||||
// Sorted lexicographically by key to allow binary search.
|
// Sorted lexicographically by key to allow binary search.
|
||||||
static constexpr EntityPair ENTITY_LOOKUP[] = {
|
static constexpr EntityPair ENTITY_LOOKUP[] = {
|
||||||
{"Æ", "Æ"}, {"Á", "Á"}, {"Â", "Â"}, {"À", "À"}, {"Α", "Α"},
|
{"Æ", "Æ"}, {"Á", "Á"}, {"Â", "Â"}, {"À", "À"}, {"Α", "Α"},
|
||||||
{"Å", "Å"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Β", "Β"}, {"Ç", "Ç"},
|
{"Å", "Å"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Β", "Β"}, {"Ç", "Ç"},
|
||||||
{"Χ", "Χ"}, {"‡", "‡"}, {"Δ", "Δ"}, {"Ð", "Ð"}, {"É", "É"},
|
{"Χ", "Χ"}, {"‡", "‡"}, {"Δ", "Δ"}, {"Ð", "Ð"}, {"É", "É"},
|
||||||
{"Ê", "Ê"}, {"È", "È"}, {"Ε", "Ε"}, {"Η", "Η"}, {"Ë", "Ë"},
|
{"Ê", "Ê"}, {"È", "È"}, {"Ε", "Ε"}, {"Η", "Η"}, {"Ë", "Ë"},
|
||||||
{"Γ", "Γ"}, {"Í", "Í"}, {"Î", "Î"}, {"Ì", "Ì"}, {"Ι", "Ι"},
|
{"Γ", "Γ"}, {"Í", "Í"}, {"Î", "Î"}, {"Ì", "Ì"}, {"Ι", "Ι"},
|
||||||
{"Ï", "Ï"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"}, {"Ñ", "Ñ"},
|
{"Ï", "Ï"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"}, {"Ñ", "Ñ"},
|
||||||
{"Ν", "Ν"}, {"Œ", "Œ"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Ò", "Ò"},
|
{"Ν", "Ν"}, {"Œ", "Œ"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Ò", "Ò"},
|
||||||
{"Ω", "Ω"}, {"Ο", "Ο"}, {"Ø", "Ø"}, {"Õ", "Õ"}, {"Ö", "Ö"},
|
{"Ω", "Ω"}, {"Ο", "Ο"}, {"Ø", "Ø"}, {"Õ", "Õ"}, {"Ö", "Ö"},
|
||||||
{"Φ", "Φ"}, {"Π", "Π"}, {"″", "″"}, {"Ψ", "Ψ"}, {"Ρ", "Ρ"},
|
{"Φ", "Φ"}, {"Π", "Π"}, {"″", "″"}, {"Ψ", "Ψ"}, {"Ρ", "Ρ"},
|
||||||
{"Š", "Š"}, {"Σ", "Σ"}, {"Þ", "Þ"}, {"Τ", "Τ"}, {"Θ", "Θ"},
|
{"Š", "Š"}, {"Σ", "Σ"}, {"Þ", "Þ"}, {"Τ", "Τ"}, {"Θ", "Θ"},
|
||||||
{"Ú", "Ú"}, {"Û", "Û"}, {"Ù", "Ù"}, {"Υ", "Υ"}, {"Ü", "Ü"},
|
{"Ú", "Ú"}, {"Û", "Û"}, {"Ù", "Ù"}, {"Υ", "Υ"}, {"Ü", "Ü"},
|
||||||
{"Ξ", "Ξ"}, {"Ý", "Ý"}, {"Ÿ", "Ÿ"}, {"Ζ", "Ζ"}, {"á", "á"},
|
{"Ξ", "Ξ"}, {"Ý", "Ý"}, {"Ÿ", "Ÿ"}, {"Ζ", "Ζ"}, {"á", "á"},
|
||||||
{"â", "â"}, {"´", "´"}, {"æ", "æ"}, {"à", "à"}, {"α", "α"},
|
{"â", "â"}, {"´", "´"}, {"æ", "æ"}, {"à", "à"}, {"ℵ", "ℵ"},
|
||||||
{"&", "&"}, {"∧", "∧"}, {"∠", "∠"}, {"å", "å"}, {"≈", "≈"},
|
{"α", "α"}, {"&", "&"}, {"∧", "∧"}, {"∠", "∠"}, {"å", "å"},
|
||||||
{"ã", "ã"}, {"ä", "ä"}, {"„", "„"}, {"β", "β"}, {"¦", "¦"},
|
{"≈", "≈"}, {"ã", "ã"}, {"ä", "ä"}, {"„", "„"}, {"β", "β"},
|
||||||
{"•", "•"}, {"∩", "∩"}, {"ç", "ç"}, {"¸", "¸"}, {"¢", "¢"},
|
{"¦", "¦"}, {"•", "•"}, {"∩", "∩"}, {"ç", "ç"}, {"¸", "¸"},
|
||||||
{"χ", "χ"}, {"ˆ", "ˆ"}, {"♣", "♣"}, {"≅", "≅"}, {"©", "©"},
|
{"¢", "¢"}, {"χ", "χ"}, {"ˆ", "ˆ"}, {"♣", "♣"}, {"≅", "≅"},
|
||||||
{"↵", "↵"}, {"∪", "∪"}, {"¤", "¤"}, {"†", "†"}, {"↓", "↓"},
|
{"©", "©"}, {"↵", "↵"}, {"∪", "∪"}, {"¤", "¤"}, {"⇓", "⇓"},
|
||||||
{"°", "°"}, {"δ", "δ"}, {"♦", "♦"}, {"÷", "÷"}, {"é", "é"},
|
{"†", "†"}, {"↓", "↓"}, {"°", "°"}, {"δ", "δ"}, {"♦", "♦"},
|
||||||
{"ê", "ê"}, {"è", "è"}, {"∅", "∅"}, {" ", " "}, {" ", " "},
|
{"÷", "÷"}, {"é", "é"}, {"ê", "ê"}, {"è", "è"}, {"∅", "∅"},
|
||||||
{"ε", "ε"}, {"≡", "≡"}, {"η", "η"}, {"ð", "ð"}, {"ë", "ë"},
|
{" ", " "}, {" ", " "}, {"ε", "ε"}, {"≡", "≡"}, {"η", "η"},
|
||||||
{"€", "€"}, {"∃", "∃"}, {"ƒ", "ƒ"}, {"∀", "∀"}, {"½", "½"},
|
{"ð", "ð"}, {"ë", "ë"}, {"€", "€"}, {"∃", "∃"}, {"ƒ", "ƒ"},
|
||||||
{"¼", "¼"}, {"¾", "¾"}, {"⁄", "⁄"}, {"γ", "γ"}, {"≥", "≥"},
|
{"∀", "∀"}, {"½", "½"}, {"¼", "¼"}, {"¾", "¾"}, {"⁄", "⁄"},
|
||||||
{">", ">"}, {"↔", "↔"}, {"♥", "♥"}, {"…", "…"}, {"í", "í"},
|
{"γ", "γ"}, {"≥", "≥"}, {">", ">"}, {"⇔", "⇔"}, {"↔", "↔"},
|
||||||
{"î", "î"}, {"¡", "¡"}, {"ì", "ì"}, {"∞", "∞"}, {"∫", "∫"},
|
{"♥", "♥"}, {"…", "…"}, {"í", "í"}, {"î", "î"}, {"¡", "¡"},
|
||||||
{"ι", "ι"}, {"¿", "¿"}, {"∈", "∈"}, {"ï", "ï"}, {"κ", "κ"},
|
{"ì", "ì"}, {"ℑ", "ℑ"}, {"∞", "∞"}, {"∫", "∫"}, {"ι", "ι"},
|
||||||
{"λ", "λ"}, {"«", "«"}, {"←", "←"}, {"⌈", "⌈"}, {"“", "\u201C"},
|
{"¿", "¿"}, {"∈", "∈"}, {"ï", "ï"}, {"κ", "κ"}, {"⇐", "⇐"},
|
||||||
{"≤", "≤"}, {"⌊", "⌊"}, {"∗", "∗"}, {"◊", "◊"}, {"‎", "\u200E"},
|
{"λ", "λ"}, {"⟨", "〈"}, {"«", "«"}, {"←", "←"}, {"⌈", "⌈"},
|
||||||
{"‹", "‹"}, {"‘", "\u2018"}, {"<", "<"}, {"¯", "¯"}, {"—", "—"},
|
{"“", "\u201C"}, {"≤", "≤"}, {"⌊", "⌊"}, {"∗", "∗"}, {"◊", "◊"},
|
||||||
{"µ", "µ"}, {"−", "−"}, {"μ", "μ"}, {"∇", "∇"}, {" ", "\xC2\xA0"},
|
{"‎", "\u200E"}, {"‹", "‹"}, {"‘", "\u2018"}, {"<", "<"}, {"¯", "¯"},
|
||||||
{"–", "–"}, {"≠", "≠"}, {"∋", "∋"}, {"¬", "¬"}, {"∉", "∉"},
|
{"—", "—"}, {"µ", "µ"}, {"·", "·"}, {"−", "−"}, {"μ", "μ"},
|
||||||
{"⊄", "⊄"}, {"ñ", "ñ"}, {"ν", "ν"}, {"ó", "ó"}, {"ô", "ô"},
|
{"∇", "∇"}, {" ", "\xC2\xA0"}, {"–", "–"}, {"≠", "≠"}, {"∋", "∋"},
|
||||||
{"œ", "œ"}, {"ò", "ò"}, {"‾", "‾"}, {"ω", "ω"}, {"ο", "ο"},
|
{"¬", "¬"}, {"∉", "∉"}, {"⊄", "⊄"}, {"ñ", "ñ"}, {"ν", "ν"},
|
||||||
{"⊕", "⊕"}, {"∨", "∨"}, {"ª", "ª"}, {"º", "º"}, {"ø", "ø"},
|
{"ó", "ó"}, {"ô", "ô"}, {"œ", "œ"}, {"ò", "ò"}, {"‾", "‾"},
|
||||||
{"õ", "õ"}, {"⊗", "⊗"}, {"ö", "ö"}, {"¶", "¶"}, {"∂", "∂"},
|
{"ω", "ω"}, {"ο", "ο"}, {"⊕", "⊕"}, {"∨", "∨"}, {"ª", "ª"},
|
||||||
{"‰", "‰"}, {"⊥", "⊥"}, {"φ", "φ"}, {"π", "π"}, {"ϖ", "ϖ"},
|
{"º", "º"}, {"ø", "ø"}, {"õ", "õ"}, {"⊗", "⊗"}, {"ö", "ö"},
|
||||||
{"±", "±"}, {"£", "£"}, {"′", "′"}, {"∏", "∏"}, {"∝", "∝"},
|
{"¶", "¶"}, {"∂", "∂"}, {"‰", "‰"}, {"⊥", "⊥"}, {"φ", "φ"},
|
||||||
{"ψ", "ψ"}, {""", "\""}, {"√", "√"}, {"»", "»"}, {"→", "→"},
|
{"π", "π"}, {"ϖ", "ϖ"}, {"±", "±"}, {"£", "£"}, {"′", "′"},
|
||||||
{"⌉", "⌉"}, {"”", "\u201D"}, {"®", "®"}, {"⌋", "⌋"}, {"ρ", "ρ"},
|
{"∏", "∏"}, {"∝", "∝"}, {"ψ", "ψ"}, {""", "\""}, {"⇒", "⇒"},
|
||||||
{"‏", "\u200F"}, {"›", "›"}, {"’", "\u2019"}, {"‚", "‚"}, {"š", "š"},
|
{"√", "√"}, {"⟩", "〉"}, {"»", "»"}, {"→", "→"}, {"⌉", "⌉"},
|
||||||
{"⋅", "⋅"}, {"§", "§"}, {"­", "\xC2\xAD"}, {"σ", "σ"}, {"ς", "ς"},
|
{"”", "\u201D"}, {"ℜ", "\u211C"}, {"®", "®"}, {"⌋", "⌋"}, {"ρ", "ρ"},
|
||||||
{"∼", "∼"}, {"♠", "♠"}, {"⊂", "⊂"}, {"⊆", "⊆"}, {"∑", "∑"},
|
{"‏", "\u200F"}, {"›", "›"}, {"’", "\u2019"}, {"‚", "‚"}, {"š", "š"},
|
||||||
{"¹", "¹"}, {"²", "²"}, {"³", "³"}, {"⊃", "⊃"}, {"⊇", "⊇"},
|
{"⋅", "⋅"}, {"§", "§"}, {"­", "\xC2\xAD"}, {"σ", "σ"}, {"ς", "ς"},
|
||||||
{"ß", "ß"}, {"τ", "τ"}, {"∴", "∴"}, {"θ", "θ"}, {"ϑ", "ϑ"},
|
{"∼", "∼"}, {"♠", "♠"}, {"⊂", "⊂"}, {"⊆", "⊆"}, {"∑", "∑"},
|
||||||
{" ", " "}, {"þ", "þ"}, {"˜", "˜"}, {"×", "×"}, {"™", "™"},
|
{"¹", "¹"}, {"²", "²"}, {"³", "³"}, {"⊃", "⊃"}, {"⊇", "⊇"},
|
||||||
{"ú", "ú"}, {"↑", "↑"}, {"û", "û"}, {"ù", "ù"}, {"¨", "¨"},
|
{"ß", "ß"}, {"τ", "τ"}, {"∴", "∴"}, {"θ", "θ"}, {"ϑ", "ϑ"},
|
||||||
{"ϒ", "ϒ"}, {"υ", "υ"}, {"ü", "ü"}, {"ξ", "ξ"}, {"ý", "ý"},
|
{" ", " "}, {"þ", "þ"}, {"˜", "˜"}, {"×", "×"}, {"™", "™"},
|
||||||
{"¥", "¥"}, {"ÿ", "ÿ"}, {"ζ", "ζ"}, {"‍", "\u200D"}, {"‌", "\u200C"},
|
{"⇑", "⇑"}, {"ú", "ú"}, {"↑", "↑"}, {"û", "û"}, {"ù", "ù"},
|
||||||
|
{"¨", "¨"}, {"ϒ", "ϒ"}, {"υ", "υ"}, {"ü", "ü"}, {"℘", "℘"},
|
||||||
|
{"ξ", "ξ"}, {"ý", "ý"}, {"¥", "¥"}, {"ÿ", "ÿ"}, {"ζ", "ζ"},
|
||||||
|
{"‍", "\u200D"}, {"‌", "\u200C"},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify the table is sorted at compile time.
|
// Verify the table is sorted at compile time.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct Iso639Mapping {
|
|||||||
};
|
};
|
||||||
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||||
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
||||||
{"ukr", "uk"}, {"swe", "sv"}};
|
{"ukr", "uk"}, {"swe", "sv"}, {"fin", "fi"}};
|
||||||
|
|
||||||
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "generated/hyph-de.trie.h"
|
#include "generated/hyph-de.trie.h"
|
||||||
#include "generated/hyph-en.trie.h"
|
#include "generated/hyph-en.trie.h"
|
||||||
#include "generated/hyph-es.trie.h"
|
#include "generated/hyph-es.trie.h"
|
||||||
|
#include "generated/hyph-fi.trie.h"
|
||||||
#include "generated/hyph-fr.trie.h"
|
#include "generated/hyph-fr.trie.h"
|
||||||
#include "generated/hyph-it.trie.h"
|
#include "generated/hyph-it.trie.h"
|
||||||
#include "generated/hyph-pl.trie.h"
|
#include "generated/hyph-pl.trie.h"
|
||||||
@@ -26,8 +27,9 @@ LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
|||||||
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator finnishHyphenator(fi_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
using EntryArray = std::array<LanguageEntry, 9>;
|
using EntryArray = std::array<LanguageEntry, 10>;
|
||||||
|
|
||||||
const EntryArray& entries() {
|
const EntryArray& entries() {
|
||||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||||
@@ -38,7 +40,8 @@ const EntryArray& entries() {
|
|||||||
{"italian", "it", &italianHyphenator},
|
{"italian", "it", &italianHyphenator},
|
||||||
{"polish", "pl", &polishHyphenator},
|
{"polish", "pl", &polishHyphenator},
|
||||||
{"swedish", "sv", &swedishHyphenator},
|
{"swedish", "sv", &swedishHyphenator},
|
||||||
{"ukrainian", "uk", &ukrainianHyphenator}}};
|
{"ukrainian", "uk", &ukrainianHyphenator},
|
||||||
|
{"finnish", "fi", &finnishHyphenator}}};
|
||||||
return kEntries;
|
return kEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t fi_trie_data[] = {
|
||||||
|
0x01, 0x01, 0x16, 0x0B, 0x0C, 0x17, 0x0C, 0x15, 0x0C, 0x0B, 0x16, 0x29, 0x2B, 0x20, 0x1F, 0x0C,
|
||||||
|
0x33, 0x02, 0x0B, 0x02, 0x0B, 0x0C, 0x01, 0x0C, 0x34, 0x0B, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x20,
|
||||||
|
0x0B, 0x21, 0xA0, 0x00, 0x41, 0xA0, 0x02, 0x51, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00,
|
||||||
|
0x41, 0x62, 0xFD, 0xA0, 0x01, 0xA2, 0xA1, 0x00, 0x81, 0x6F, 0xFD, 0xA3, 0x00, 0x81, 0x69, 0x6F,
|
||||||
|
0x75, 0xF8, 0xF8, 0xF8, 0x28, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72, 0xDE, 0xDE, 0xEA,
|
||||||
|
0xDE, 0xDE, 0xDE, 0xF2, 0xF7, 0x22, 0xA4, 0xB6, 0xCD, 0xCD, 0xA1, 0x00, 0x81, 0x61, 0xD9, 0x28,
|
||||||
|
0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x72, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xF6, 0xFB,
|
||||||
|
0xA2, 0x00, 0x81, 0x61, 0x65, 0xC3, 0xC3, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72,
|
||||||
|
0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xE3, 0xFF, 0xF9,
|
||||||
|
0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92,
|
||||||
|
0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0xC5, 0xFF, 0xA6, 0xFF, 0xCA, 0x47, 0x61, 0x65, 0x69,
|
||||||
|
0x6F, 0x75, 0x79, 0xC3, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76,
|
||||||
|
0xFF, 0xA9, 0xA0, 0x00, 0xF1, 0x21, 0x73, 0xFD, 0xA1, 0x00, 0x41, 0x75, 0xFD, 0xA0, 0x00, 0x81,
|
||||||
|
0x43, 0x61, 0x65, 0x69, 0xFF, 0x63, 0xFF, 0x63, 0xFF, 0x63, 0xC1, 0x01, 0xA2, 0x61, 0xFF, 0x59,
|
||||||
|
0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0x76, 0xFF, 0x42, 0xFF, 0xE8, 0xFF,
|
||||||
|
0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x75, 0xFF, 0xED, 0xFF, 0xF0, 0xFF, 0xFA, 0xA1,
|
||||||
|
0x00, 0x41, 0x73, 0xCE, 0x47, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xFB, 0xFF, 0x1E,
|
||||||
|
0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x51, 0xA0, 0x01, 0x52, 0x21, 0x6F, 0xFD,
|
||||||
|
0x22, 0x74, 0x6E, 0xFD, 0xFD, 0xA0, 0x01, 0x73, 0x21, 0x6E, 0xFD, 0x22, 0x61, 0x6F, 0xFD, 0xFA,
|
||||||
|
0x21, 0x61, 0xEA, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xF2, 0xA4, 0x00, 0x41, 0x6E, 0x6A, 0x69, 0x6C,
|
||||||
|
0xE7, 0xF2, 0xFA, 0xFD, 0x21, 0x73, 0xE1, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41, 0x64, 0xFD, 0x21,
|
||||||
|
0x74, 0xD6, 0x21, 0x73, 0xFD, 0x22, 0x65, 0x69, 0xFA, 0xFD, 0x21, 0x61, 0xCB, 0x21, 0x6E, 0xBD,
|
||||||
|
0x22, 0x74, 0x6F, 0xBD, 0xFD, 0x21, 0x69, 0xC0, 0x21, 0x61, 0xFD, 0xA4, 0x00, 0x41, 0x70, 0x73,
|
||||||
|
0x74, 0x6D, 0xEA, 0xEF, 0xF5, 0xFD, 0x21, 0x69, 0xD9, 0xA1, 0x00, 0x41, 0x6C, 0xFD, 0x47, 0x61,
|
||||||
|
0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xBB, 0xFF, 0xCC, 0xFE, 0xA4, 0xFF, 0xED, 0xFE, 0xA4,
|
||||||
|
0xFF, 0xFB, 0xFE, 0xD7, 0xA0, 0x01, 0x41, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41,
|
||||||
|
0x72, 0xFD, 0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFE, 0x80, 0xFF, 0xFB,
|
||||||
|
0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0xB3, 0xFF, 0x2B, 0xFE, 0x94, 0x21, 0x61,
|
||||||
|
0xDC, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0xFF, 0x3E, 0xFE, 0x61, 0xFE, 0x61,
|
||||||
|
0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x94, 0xFF, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0x80, 0xFF,
|
||||||
|
0x40, 0x42, 0x6F, 0x65, 0xFF, 0x84, 0xFF, 0x47, 0x41, 0x75, 0xFF, 0x32, 0x21, 0x74, 0xFC, 0x42,
|
||||||
|
0x61, 0x6F, 0xFF, 0xFD, 0xFF, 0x36, 0xA4, 0x00, 0x41, 0x73, 0x6C, 0x6A, 0x70, 0xE4, 0xEB, 0xF9,
|
||||||
|
0xF2, 0x41, 0x79, 0xFF, 0x24, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x41, 0x73, 0xFD,
|
||||||
|
0x42, 0x2E, 0x6E, 0xFF, 0x15, 0xFF, 0x15, 0x21, 0x61, 0xF9, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x41,
|
||||||
|
0x64, 0xFD, 0x41, 0x65, 0xFE, 0xF8, 0x21, 0x6A, 0xFC, 0x42, 0x6B, 0x74, 0xFE, 0xFC, 0xFE, 0xFC,
|
||||||
|
0x21, 0x73, 0xF9, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0x41, 0x68, 0x70, 0x73, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||||
|
0xFF, 0x24, 0x41, 0x74, 0xFF, 0x23, 0xC2, 0x00, 0x41, 0x72, 0x68, 0xFF, 0x30, 0xFF, 0xFC, 0xA0,
|
||||||
|
0x00, 0x52, 0x21, 0x72, 0xFD, 0x21, 0x69, 0xFA, 0x21, 0x6C, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x68,
|
||||||
|
0xFD, 0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0x70, 0x63, 0xFF, 0x95, 0xFF, 0xAA,
|
||||||
|
0xFF, 0xBC, 0xFF, 0xD5, 0xFD, 0xC1, 0xFF, 0xE5, 0xFD, 0xF4, 0xFF, 0xF1, 0xFF, 0xF7, 0xFF, 0xFD,
|
||||||
|
0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x73, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD,
|
||||||
|
0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xD5, 0xFF, 0xDE, 0xA0, 0x00, 0x92, 0xA0, 0x00, 0xB2, 0xA0,
|
||||||
|
0x01, 0x01, 0xC3, 0x00, 0x61, 0x69, 0x65, 0x79, 0xFE, 0x20, 0xFE, 0x20, 0xFF, 0xFD, 0x22, 0xA4,
|
||||||
|
0xB6, 0xF4, 0xAD, 0x25, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0xA8, 0xE6, 0xE6, 0xE9, 0xFB, 0x42, 0xB6,
|
||||||
|
0xA4, 0xFF, 0x9D, 0xFF, 0x9D, 0x46, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0x65, 0xFF, 0x96, 0xFF, 0xD4,
|
||||||
|
0xFF, 0xD4, 0xFF, 0xD7, 0xFF, 0xF9, 0xFF, 0xD7, 0x22, 0xA4, 0xB6, 0xDB, 0xED, 0xA0, 0x00, 0x72,
|
||||||
|
0xA0, 0x00, 0x71, 0x21, 0xA4, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xA4, 0xFD, 0x21, 0x69, 0xF4, 0xA0,
|
||||||
|
0x01, 0x22, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x26, 0x61, 0x6F, 0x75, 0xC3,
|
||||||
|
0x65, 0x6C, 0xE2, 0xE2, 0xE2, 0xEE, 0xF1, 0xFD, 0x22, 0xA4, 0xB6, 0xD8, 0xD8, 0x21, 0x61, 0xD3,
|
||||||
|
0xA0, 0x00, 0xB1, 0x24, 0x75, 0x69, 0x65, 0x6F, 0xCD, 0xCD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F,
|
||||||
|
0x75, 0xF4, 0xF4, 0xF4, 0xF4, 0x25, 0x79, 0xC3, 0x61, 0x75, 0x69, 0xBB, 0xE3, 0xE8, 0xEE, 0xF7,
|
||||||
|
0xA0, 0x00, 0xD2, 0x22, 0xA4, 0xB6, 0xFD, 0xFD, 0x44, 0x61, 0x65, 0x6F, 0x69, 0xFF, 0x64, 0xFF,
|
||||||
|
0x64, 0xFF, 0x64, 0xFF, 0x64, 0x42, 0x65, 0x61, 0xFF, 0x9B, 0xFF, 0xCB, 0x21, 0x65, 0xC4, 0x22,
|
||||||
|
0x61, 0x75, 0xC1, 0xC1, 0xA0, 0x02, 0x32, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x49, 0x79, 0xC3,
|
||||||
|
0x75, 0x61, 0x65, 0x69, 0x6F, 0x73, 0x6C, 0xFF, 0x80, 0xFF, 0xD6, 0xFF, 0xDB, 0xFF, 0xB0, 0xFF,
|
||||||
|
0xE8, 0xFF, 0xEF, 0xFF, 0xF2, 0xFD, 0x70, 0xFF, 0xFD, 0x44, 0x69, 0x65, 0x6F, 0x75, 0xFF, 0x23,
|
||||||
|
0xFF, 0x23, 0xFF, 0x23, 0xFF, 0x23, 0x43, 0x75, 0x61, 0x65, 0xFF, 0x5A, 0xFF, 0x8A, 0xFF, 0x8A,
|
||||||
|
0x41, 0x76, 0xFF, 0x5F, 0x21, 0x61, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD,
|
||||||
|
0x21, 0x65, 0xFD, 0x43, 0x69, 0x6F, 0x6B, 0xFF, 0xF1, 0xFD, 0xF7, 0xFF, 0xFD, 0xA0, 0x01, 0xA4,
|
||||||
|
0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x43, 0x6E, 0x74, 0x6B, 0xFC, 0x7D, 0xFC, 0x7D, 0xFF, 0xFD,
|
||||||
|
0x41, 0x69, 0xFC, 0x73, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x48, 0xC3, 0x61, 0x75,
|
||||||
|
0x65, 0x6F, 0x69, 0x6C, 0x73, 0xFF, 0x3C, 0xFF, 0xAD, 0xFF, 0xBA, 0xFF, 0x20, 0xFF, 0x20, 0xFF,
|
||||||
|
0x50, 0xFF, 0xD7, 0xFF, 0xFD, 0x44, 0x61, 0x69, 0x75, 0x79, 0xFE, 0xB7, 0xFE, 0xB7, 0xFE, 0xB7,
|
||||||
|
0xFE, 0xB7, 0x42, 0x61, 0x69, 0xFE, 0xEE, 0xFE, 0xEE, 0x42, 0x75, 0x61, 0xFE, 0xE7, 0xFF, 0x17,
|
||||||
|
0x42, 0xA4, 0xB6, 0xFE, 0xE6, 0xFF, 0x30, 0x24, 0x65, 0x61, 0x75, 0xC3, 0xDE, 0xEB, 0xF2, 0xF9,
|
||||||
|
0x43, 0x61, 0x65, 0x6F, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0x42, 0x61, 0x75, 0xFE, 0xC6, 0xFE,
|
||||||
|
0xC6, 0x44, 0x75, 0x61, 0x65, 0x6F, 0xFE, 0xBF, 0xFE, 0xEF, 0xFE, 0xEF, 0xFE, 0xEF, 0x41, 0xB6,
|
||||||
|
0xFE, 0xB2, 0x21, 0xC3, 0xFC, 0x42, 0xA4, 0xB6, 0xFE, 0xB1, 0xFF, 0xFD, 0x43, 0x61, 0x6F, 0x79,
|
||||||
|
0xFE, 0xD4, 0xFE, 0xD4, 0xFE, 0xD4, 0x42, 0x61, 0x65, 0xFE, 0x56, 0xFE, 0x56, 0x26, 0x69, 0x61,
|
||||||
|
0x75, 0xC3, 0x65, 0x6F, 0xC3, 0xCD, 0xD4, 0xE8, 0xEF, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0xA4, 0xFD,
|
||||||
|
0xA0, 0x01, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x02, 0x03, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x75, 0xFD, 0x23, 0xC3, 0x79, 0x73, 0xE2,
|
||||||
|
0xEE, 0xFD, 0x41, 0x72, 0xFD, 0xD9, 0x42, 0x6C, 0x68, 0xFC, 0x47, 0xFF, 0xFC, 0xC1, 0x00, 0x81,
|
||||||
|
0x69, 0xFB, 0xA6, 0x21, 0x76, 0xFA, 0x59, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0xC3, 0x79, 0x6F, 0x75, 0x61, 0x65, 0x69, 0x2E, 0x63, 0x71,
|
||||||
|
0xFB, 0xAE, 0xFB, 0xC9, 0xFB, 0xE1, 0xFB, 0xFA, 0xFC, 0x16, 0xFC, 0x16, 0xFC, 0x4A, 0xFC, 0x6E,
|
||||||
|
0xFC, 0x16, 0xFC, 0xE8, 0xFD, 0x0C, 0xFD, 0x2B, 0xFD, 0xCB, 0xFD, 0xEA, 0xFC, 0x16, 0xFE, 0x42,
|
||||||
|
0xFE, 0x65, 0xFE, 0x8F, 0xFE, 0xC7, 0xFF, 0x36, 0xFF, 0x71, 0xFF, 0xB7, 0xFF, 0xE5, 0xFF, 0xF0,
|
||||||
|
0xFF, 0xFD,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns fi_patterns = {
|
||||||
|
0x496u,
|
||||||
|
fi_trie_data,
|
||||||
|
sizeof(fi_trie_data),
|
||||||
|
};
|
||||||
@@ -34,7 +34,8 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
|||||||
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
||||||
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
||||||
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||||
constexpr const char* IMAGE_TAGS[] = {"img"};
|
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
|
||||||
|
constexpr const char* IMAGE_TAGS[] = {"img", "image"};
|
||||||
constexpr const char* SKIP_TAGS[] = {"head"};
|
constexpr const char* SKIP_TAGS[] = {"head"};
|
||||||
|
|
||||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||||
@@ -89,13 +90,50 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update effective bold/italic/underline based on block style and inline style stack
|
EpdFontFamily::Style ChapterHtmlSlimParser::fontStyleForTextDecoration(const CssTextDecoration decoration) {
|
||||||
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR;
|
||||||
|
if ((decoration & CssTextDecoration::Underline) != CssTextDecoration::None) {
|
||||||
|
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::UNDERLINE);
|
||||||
|
}
|
||||||
|
if ((decoration & CssTextDecoration::LineThrough) != CssTextDecoration::None) {
|
||||||
|
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::STRIKETHROUGH);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css) {
|
||||||
|
if (css.hasTextDecoration()) {
|
||||||
|
entry.hasTextDecoration = true;
|
||||||
|
entry.textDecoration = css.textDecoration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::pushDecorationStyleEntry(const CssTextDecoration defaultDecoration,
|
||||||
|
const CssStyle& cssStyle) {
|
||||||
|
StyleStackEntry entry;
|
||||||
|
entry.depth = depth;
|
||||||
|
entry.hasTextDecoration = true;
|
||||||
|
entry.textDecoration = cssStyle.hasTextDecoration() ? cssStyle.textDecoration : defaultDecoration;
|
||||||
|
if (cssStyle.hasFontWeight()) {
|
||||||
|
entry.hasBold = true;
|
||||||
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasFontStyle()) {
|
||||||
|
entry.hasItalic = true;
|
||||||
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
|
}
|
||||||
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
|
inlineStyleStack.push_back(entry);
|
||||||
|
updateEffectiveInlineStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update effective bold/italic/decorations based on block style and inline style stack
|
||||||
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||||
// Start with block-level styles
|
// Start with block-level styles
|
||||||
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
effectiveUnderline =
|
effectiveTextDecoration =
|
||||||
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
currentCssStyle.hasTextDecoration() ? currentCssStyle.textDecoration : CssTextDecoration::None;
|
||||||
effectiveDirectionDefined = currentCssStyle.hasDirection();
|
effectiveDirectionDefined = currentCssStyle.hasDirection();
|
||||||
effectiveDirection = currentCssStyle.direction;
|
effectiveDirection = currentCssStyle.direction;
|
||||||
effectiveSup = false;
|
effectiveSup = false;
|
||||||
@@ -109,8 +147,10 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
|||||||
if (entry.hasItalic) {
|
if (entry.hasItalic) {
|
||||||
effectiveItalic = entry.italic;
|
effectiveItalic = entry.italic;
|
||||||
}
|
}
|
||||||
if (entry.hasUnderline) {
|
// CSS line decorations propagate through descendants; child entries add
|
||||||
effectiveUnderline = entry.underline;
|
// their own lines but cannot cancel an ancestor's already active line.
|
||||||
|
if (entry.hasTextDecoration) {
|
||||||
|
effectiveTextDecoration = effectiveTextDecoration | entry.textDecoration;
|
||||||
}
|
}
|
||||||
if (entry.hasDirection) {
|
if (entry.hasDirection) {
|
||||||
effectiveDirectionDefined = true;
|
effectiveDirectionDefined = true;
|
||||||
@@ -164,7 +204,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
// Determine font style from depth-based tracking and CSS effective style
|
// Determine font style from depth-based tracking and CSS effective style
|
||||||
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
||||||
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||||
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
|
||||||
|
|
||||||
// Combine style flags using bitwise OR
|
// Combine style flags using bitwise OR
|
||||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
@@ -174,9 +213,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
if (isItalic) {
|
if (isItalic) {
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
||||||
}
|
}
|
||||||
if (isUnderline) {
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | fontStyleForTextDecoration(effectiveTextDecoration));
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
|
||||||
}
|
|
||||||
if (effectiveSup) {
|
if (effectiveSup) {
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
|
||||||
} else if (effectiveSub) {
|
} else if (effectiveSub) {
|
||||||
@@ -188,6 +225,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||||
partWordBufferIndex = 0;
|
partWordBufferIndex = 0;
|
||||||
nextWordContinues = false;
|
nextWordContinues = false;
|
||||||
|
listItemBulletOnly = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start a new text block if needed
|
// start a new text block if needed
|
||||||
@@ -202,8 +240,28 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
|||||||
// open. Merge those into the new style so the first child in a container inherits
|
// open. Merge those into the new style so the first child in a container inherits
|
||||||
// the container's vertical spacing.
|
// the container's vertical spacing.
|
||||||
const auto style = currentTextBlock->getBlockStyle();
|
const auto style = currentTextBlock->getBlockStyle();
|
||||||
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
BlockStyle incoming = blockStyle;
|
||||||
|
if (style.fromBrElement) {
|
||||||
|
// The empty block was created by a <br> section separator. Inject a full line of
|
||||||
|
// blank space before the following paragraph so the scene/section break is visible.
|
||||||
|
// This only fires when the <br> block stayed empty (i.e. no inline text was added).
|
||||||
|
const int16_t lineHeight = static_cast<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
|
||||||
|
incoming.marginTop = static_cast<int16_t>(incoming.marginTop + lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(incoming, BlockStyle::CombineAxis::Vertical));
|
||||||
|
|
||||||
|
flushPendingAnchor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <li> added a bullet as the first word, making the block non-empty. When a nested
|
||||||
|
// block-level child (<p>, <div>, etc.) opens, reuse the block instead of flushing
|
||||||
|
// the bullet to its own line. The bullet stays inline with the child's text.
|
||||||
|
if (listItemBulletOnly) {
|
||||||
|
const auto style = currentTextBlock->getBlockStyle();
|
||||||
|
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
||||||
|
listItemBulletOnly = false;
|
||||||
flushPendingAnchor();
|
flushPendingAnchor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -215,6 +273,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
|||||||
flushPendingAnchor();
|
flushPendingAnchor();
|
||||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
||||||
wordsExtractedInBlock = 0;
|
wordsExtractedInBlock = 0;
|
||||||
|
listItemBulletOnly = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
||||||
@@ -317,6 +376,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
const bool isTocAnchor =
|
const bool isTocAnchor =
|
||||||
std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end();
|
std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end();
|
||||||
if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) {
|
if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) {
|
||||||
|
// Flush a displaced anchor before overwriting. Consecutive non-block elements
|
||||||
|
// (e.g. <aside id="fn1">text</aside><aside id="fn2">) with no intervening block
|
||||||
|
// never trigger startNewTextBlock, so fn1 gets silently overwritten. That leaves
|
||||||
|
// fn1 missing from the anchor map -> getPageForAnchor returns nullopt -> reader
|
||||||
|
// lands at page 0 (section start) instead of the footnote.
|
||||||
|
if (!self->pendingAnchorId.empty()) {
|
||||||
|
self->flushPendingAnchor();
|
||||||
|
}
|
||||||
self->pendingAnchorId = idValue;
|
self->pendingAnchorId = idValue;
|
||||||
}
|
}
|
||||||
} else if (strcmp(atts[i], "dir") == 0) {
|
} else if (strcmp(atts[i], "dir") == 0) {
|
||||||
@@ -412,14 +479,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
headerStyle.bold = false;
|
headerStyle.bold = false;
|
||||||
headerStyle.hasItalic = true;
|
headerStyle.hasItalic = true;
|
||||||
headerStyle.italic = true;
|
headerStyle.italic = true;
|
||||||
headerStyle.hasUnderline = true;
|
|
||||||
headerStyle.underline = false;
|
|
||||||
self->inlineStyleStack.push_back(headerStyle);
|
self->inlineStyleStack.push_back(headerStyle);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
|
const CssTextDecoration savedTextDecoration = self->effectiveTextDecoration;
|
||||||
|
self->effectiveTextDecoration = CssTextDecoration::None;
|
||||||
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
|
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
|
self->effectiveTextDecoration = savedTextDecoration;
|
||||||
self->nextWordContinues = false;
|
self->nextWordContinues = false;
|
||||||
self->inlineStyleStack.pop_back();
|
self->inlineStyleStack.pop_back();
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -440,11 +508,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "src") == 0) {
|
if (strcmp(atts[i], "src") == 0) {
|
||||||
src = atts[i + 1];
|
src = atts[i + 1];
|
||||||
|
} else if (src.empty() && (strcmp(atts[i], "href") == 0 || strcmp(atts[i], "xlink:href") == 0)) {
|
||||||
|
src = atts[i + 1];
|
||||||
} else if (strcmp(atts[i], "alt") == 0) {
|
} else if (strcmp(atts[i], "alt") == 0) {
|
||||||
alt = atts[i + 1];
|
alt = atts[i + 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t fragmentPos = src.find('#');
|
||||||
|
if (fragmentPos != std::string::npos) {
|
||||||
|
src.resize(fragmentPos);
|
||||||
|
}
|
||||||
|
|
||||||
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
|
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
|
||||||
if (self->imageRendering == 2) {
|
if (self->imageRendering == 2) {
|
||||||
self->skipUntilDepth = self->depth;
|
self->skipUntilDepth = self->depth;
|
||||||
@@ -452,19 +527,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip image if CSS display:none
|
|
||||||
if (self->cssParser) {
|
|
||||||
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
|
|
||||||
if (!styleAttr.empty()) {
|
|
||||||
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
|
||||||
}
|
|
||||||
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
|
|
||||||
self->skipUntilDepth = self->depth;
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!src.empty() && self->imageRendering != 1) {
|
if (!src.empty() && self->imageRendering != 1) {
|
||||||
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||||
|
|
||||||
@@ -488,24 +550,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||||
cachedImageFile.flush();
|
cachedImageFile.flush();
|
||||||
cachedImageFile.close();
|
cachedImageFile.close();
|
||||||
delay(50); // Give SD card time to sync
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extractSuccess) {
|
if (extractSuccess) {
|
||||||
// Get image dimensions
|
// Get image dimensions, retrying to absorb SD-card sync latency on slow
|
||||||
|
// cards. Replaces a blanket delay(50) that cost ~50ms on every image, and
|
||||||
|
// closes the silent-drop bug where a single getDimensions failure was fatal.
|
||||||
ImageDimensions dims = {0, 0};
|
ImageDimensions dims = {0, 0};
|
||||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||||
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
bool gotDimensions = false;
|
||||||
|
for (int attempt = 0; attempt < 3 && !gotDimensions; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
delay(50); // Give a slow SD card time to finish syncing before retrying
|
||||||
|
}
|
||||||
|
gotDimensions = decoder && decoder->getDimensions(cachedImagePath, dims);
|
||||||
|
}
|
||||||
|
if (gotDimensions) {
|
||||||
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||||
|
|
||||||
int displayWidth = 0;
|
int displayWidth = 0;
|
||||||
int displayHeight = 0;
|
int displayHeight = 0;
|
||||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||||
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
|
const CssStyle& imgStyle = cssStyle;
|
||||||
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
|
|
||||||
if (!styleAttr.empty()) {
|
|
||||||
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
|
||||||
}
|
|
||||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||||
|
|
||||||
@@ -752,12 +818,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->currentFootnote.number[0] = '\0';
|
self->currentFootnote.number[0] = '\0';
|
||||||
self->currentFootnoteLinkTextLen = 0;
|
self->currentFootnoteLinkTextLen = 0;
|
||||||
|
|
||||||
// Apply underline style to visually indicate the link
|
// Apply underline style to visually indicate the link.
|
||||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
|
||||||
StyleStackEntry entry;
|
StyleStackEntry entry;
|
||||||
entry.depth = self->depth;
|
entry.depth = self->depth;
|
||||||
entry.hasUnderline = true;
|
entry.hasTextDecoration = true;
|
||||||
entry.underline = true;
|
entry.textDecoration = CssTextDecoration::Underline;
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -810,7 +875,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
self->startNewTextBlock(self->blockStyleStack.back().withoutBottom());
|
// Tag the new block so startNewTextBlock can inject a full line-height gap if
|
||||||
|
// the block remains empty (i.e. <br> is a section separator between paragraphs).
|
||||||
|
// If the block gets text added before the next block opens it becomes non-empty,
|
||||||
|
// goes through makePages() normally, and the flag has no effect (inline <br> case).
|
||||||
|
BlockStyle brStyle =
|
||||||
|
self->currentTextBlock ? self->currentTextBlock->getBlockStyle() : self->blockStyleStack.back();
|
||||||
|
brStyle.fromBrElement = true;
|
||||||
|
self->startNewTextBlock(brStyle);
|
||||||
} else {
|
} else {
|
||||||
self->currentCssStyle = cssStyle;
|
self->currentCssStyle = cssStyle;
|
||||||
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
|
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
|
||||||
@@ -821,6 +893,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
|
|
||||||
if (strcmp(name, "li") == 0) {
|
if (strcmp(name, "li") == 0) {
|
||||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||||
|
self->listItemBulletOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
||||||
@@ -829,23 +902,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
self->nextWordContinues = true;
|
self->nextWordContinues = true;
|
||||||
}
|
}
|
||||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
self->pushDecorationStyleEntry(CssTextDecoration::Underline, cssStyle);
|
||||||
// Push inline style entry for underline tag
|
} else if (matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS))) {
|
||||||
StyleStackEntry entry;
|
// Flush buffer before style change so preceding text gets current style
|
||||||
entry.depth = self->depth; // Track depth for matching pop
|
if (self->partWordBufferIndex > 0) {
|
||||||
entry.hasUnderline = true;
|
self->flushPartWordBuffer();
|
||||||
entry.underline = true;
|
self->nextWordContinues = true;
|
||||||
if (cssStyle.hasFontWeight()) {
|
|
||||||
entry.hasBold = true;
|
|
||||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
|
||||||
}
|
}
|
||||||
if (cssStyle.hasFontStyle()) {
|
self->pushDecorationStyleEntry(CssTextDecoration::LineThrough, cssStyle);
|
||||||
entry.hasItalic = true;
|
|
||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
|
||||||
self->inlineStyleStack.push_back(entry);
|
|
||||||
self->updateEffectiveInlineStyle();
|
|
||||||
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
|
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
|
||||||
// Flush buffer before style change so preceding text gets current style
|
// Flush buffer before style change so preceding text gets current style
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
@@ -862,10 +926,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.hasItalic = true;
|
entry.hasItalic = true;
|
||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
applyTextDecorationToEntry(entry, cssStyle);
|
||||||
entry.hasUnderline = true;
|
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -885,10 +946,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.hasBold = true;
|
entry.hasBold = true;
|
||||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
applyTextDecorationToEntry(entry, cssStyle);
|
||||||
entry.hasUnderline = true;
|
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -927,10 +985,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.hasItalic = true;
|
entry.hasItalic = true;
|
||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
applyTextDecorationToEntry(entry, cssStyle);
|
||||||
entry.hasUnderline = true;
|
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
||||||
}
|
|
||||||
applyDirectionToEntry(entry, cssStyle);
|
applyDirectionToEntry(entry, cssStyle);
|
||||||
if (cssStyle.hasVerticalAlign()) {
|
if (cssStyle.hasVerticalAlign()) {
|
||||||
if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
|
if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
|
||||||
@@ -1142,9 +1197,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
||||||
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
||||||
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
||||||
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
|
||||||
|
|
||||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic;
|
||||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||||
const bool tableStructuralTag = isTableStructuralTag(name);
|
const bool tableStructuralTag = isTableStructuralTag(name);
|
||||||
|
|
||||||
@@ -1163,7 +1217,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
|
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
|
||||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
|
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
|
||||||
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
|
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
|
||||||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag ||
|
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) ||
|
||||||
|
matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS)) || tableStructuralTag ||
|
||||||
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
|
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
|
||||||
|
|
||||||
if (shouldFlush) {
|
if (shouldFlush) {
|
||||||
@@ -1222,11 +1277,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->italicUntilDepth = INT_MAX;
|
self->italicUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaving underline tag
|
|
||||||
if (self->underlineUntilDepth == self->depth) {
|
|
||||||
self->underlineUntilDepth = INT_MAX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop from inline style stack if we pushed an entry at this depth
|
// Pop from inline style stack if we pushed an entry at this depth
|
||||||
// This handles all inline elements: b, i, u, span, etc.
|
// This handles all inline elements: b, i, u, span, etc.
|
||||||
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
||||||
@@ -1250,10 +1300,19 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
}
|
}
|
||||||
self->blockStyleStack.pop_back();
|
self->blockStyleStack.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// </li> closes: if the bullet never got inline text (empty <li> or <li> with only
|
||||||
|
// block children that were flushed), clear the flag so the next sibling doesn't
|
||||||
|
// merge into this block.
|
||||||
|
if (strcmp(name, "li") == 0) {
|
||||||
|
self->listItemBulletOnly = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
ChapterHtmlSlimParser::~ChapterHtmlSlimParser() { abortParse(); }
|
||||||
|
|
||||||
|
bool ChapterHtmlSlimParser::beginParse() {
|
||||||
// Initialize block style stack with a root entry representing "no ancestor block elements".
|
// Initialize block style stack with a root entry representing "no ancestor block elements".
|
||||||
// The user's paragraph alignment is set as the default so child elements without explicit
|
// The user's paragraph alignment is set as the default so child elements without explicit
|
||||||
// text-align inherit it correctly through getCombinedBlockStyle.
|
// text-align inherit it correctly through getCombinedBlockStyle.
|
||||||
@@ -1271,67 +1330,78 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
paragraphAlignmentBlockStyle.alignment = align;
|
paragraphAlignmentBlockStyle.alignment = align;
|
||||||
startNewTextBlock(paragraphAlignmentBlockStyle);
|
startNewTextBlock(paragraphAlignmentBlockStyle);
|
||||||
|
|
||||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
xmlParser_ = XML_ParserCreate(nullptr);
|
||||||
int done;
|
if (!xmlParser_) {
|
||||||
|
|
||||||
if (!parser) {
|
|
||||||
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
||||||
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
||||||
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand);
|
XML_SetDefaultHandlerExpand(xmlParser_, defaultHandlerExpand);
|
||||||
|
|
||||||
HalFile file;
|
if (!Storage.openFileForRead("EHP", filepath, parseFile_)) {
|
||||||
if (!Storage.openFileForRead("EHP", filepath, file)) {
|
destroyXmlParser(xmlParser_);
|
||||||
destroyXmlParser(parser);
|
xmlParser_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file size to decide whether to show indexing popup.
|
// Get file size to decide whether to show indexing popup.
|
||||||
if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
|
if (popupFn && parseFile_.size() >= MIN_SIZE_FOR_POPUP) {
|
||||||
popupFn();
|
popupFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
XML_SetUserData(parser, this);
|
XML_SetUserData(xmlParser_, this);
|
||||||
XML_SetElementHandler(parser, startElement, endElement);
|
XML_SetElementHandler(xmlParser_, startElement, endElement);
|
||||||
XML_SetCharacterDataHandler(parser, characterData);
|
XML_SetCharacterDataHandler(xmlParser_, characterData);
|
||||||
|
|
||||||
// Compute the time taken to parse and build pages
|
parseStartTime_ = millis();
|
||||||
const uint32_t chapterStartTime = millis();
|
return true;
|
||||||
do {
|
}
|
||||||
void* const buf = XML_GetBuffer(parser, PARSE_BUFFER_SIZE);
|
|
||||||
if (!buf) {
|
|
||||||
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
|
||||||
destroyXmlParser(parser);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t len = file.read(buf, PARSE_BUFFER_SIZE);
|
ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() {
|
||||||
|
void* const buf = XML_GetBuffer(xmlParser_, PARSE_BUFFER_SIZE);
|
||||||
|
if (!buf) {
|
||||||
|
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
||||||
|
return ParseStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
if (len == 0 && file.available() > 0) {
|
const size_t len = parseFile_.read(buf, PARSE_BUFFER_SIZE);
|
||||||
LOG_ERR("EHP", "File read error");
|
|
||||||
destroyXmlParser(parser);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
done = file.available() == 0;
|
if (len == 0 && parseFile_.available() > 0) {
|
||||||
|
LOG_ERR("EHP", "File read error");
|
||||||
|
return ParseStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
const int done = parseFile_.available() == 0;
|
||||||
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
|
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
|
||||||
destroyXmlParser(parser);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} while (!done);
|
|
||||||
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - chapterStartTime);
|
|
||||||
|
|
||||||
destroyXmlParser(parser);
|
if (XML_ParseBuffer(xmlParser_, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
file.close();
|
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(xmlParser_),
|
||||||
|
XML_ErrorString(XML_GetErrorCode(xmlParser_)));
|
||||||
|
return ParseStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return done ? ParseStatus::Done : ParseStatus::More;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::abortParse() {
|
||||||
|
if (xmlParser_) {
|
||||||
|
destroyXmlParser(xmlParser_);
|
||||||
|
xmlParser_ = nullptr;
|
||||||
|
}
|
||||||
|
// Only close the file if it was successfully opened in beginParse()
|
||||||
|
if (parseFile_.isOpen()) {
|
||||||
|
parseFile_.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChapterHtmlSlimParser::finishParse() {
|
||||||
|
if (xmlParser_) {
|
||||||
|
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - parseStartTime_);
|
||||||
|
destroyXmlParser(xmlParser_);
|
||||||
|
xmlParser_ = nullptr;
|
||||||
|
}
|
||||||
|
parseFile_.close();
|
||||||
|
|
||||||
// Process last page if there is still text
|
// Process last page if there is still text
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
@@ -1349,6 +1419,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||||
|
if (!beginParse()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
const ParseStatus status = parseStep();
|
||||||
|
if (status == ParseStatus::Error) {
|
||||||
|
abortParse();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (status == ParseStatus::Done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finishParse();
|
||||||
|
}
|
||||||
|
|
||||||
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
||||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include <climits>
|
#include <climits>
|
||||||
@@ -31,7 +32,6 @@ class ChapterHtmlSlimParser {
|
|||||||
int skipUntilDepth = INT_MAX;
|
int skipUntilDepth = INT_MAX;
|
||||||
int boldUntilDepth = INT_MAX;
|
int boldUntilDepth = INT_MAX;
|
||||||
int italicUntilDepth = INT_MAX;
|
int italicUntilDepth = INT_MAX;
|
||||||
int underlineUntilDepth = INT_MAX;
|
|
||||||
// buffer for building up words from characters, will auto break if longer than this
|
// buffer for building up words from characters, will auto break if longer than this
|
||||||
// leave one char at end for null pointer
|
// leave one char at end for null pointer
|
||||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||||
@@ -60,7 +60,8 @@ class ChapterHtmlSlimParser {
|
|||||||
int depth = 0;
|
int depth = 0;
|
||||||
bool hasBold = false, bold = false;
|
bool hasBold = false, bold = false;
|
||||||
bool hasItalic = false, italic = false;
|
bool hasItalic = false, italic = false;
|
||||||
bool hasUnderline = false, underline = false;
|
bool hasTextDecoration = false;
|
||||||
|
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||||
bool hasDirection = false;
|
bool hasDirection = false;
|
||||||
CssTextDirection direction = CssTextDirection::Ltr;
|
CssTextDirection direction = CssTextDirection::Ltr;
|
||||||
bool hasSup = false, sup = false;
|
bool hasSup = false, sup = false;
|
||||||
@@ -71,7 +72,7 @@ class ChapterHtmlSlimParser {
|
|||||||
CssStyle currentCssStyle;
|
CssStyle currentCssStyle;
|
||||||
bool effectiveBold = false;
|
bool effectiveBold = false;
|
||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = false;
|
CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
|
||||||
bool effectiveDirectionDefined = false;
|
bool effectiveDirectionDefined = false;
|
||||||
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
|
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
|
||||||
bool effectiveSup = false;
|
bool effectiveSup = false;
|
||||||
@@ -79,6 +80,7 @@ class ChapterHtmlSlimParser {
|
|||||||
int tableDepth = 0;
|
int tableDepth = 0;
|
||||||
int tableRowIndex = 0;
|
int tableRowIndex = 0;
|
||||||
int tableColIndex = 0;
|
int tableColIndex = 0;
|
||||||
|
bool listItemBulletOnly = false; // true when currentTextBlock has only the <li> bullet
|
||||||
|
|
||||||
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
||||||
int completedPageCount = 0;
|
int completedPageCount = 0;
|
||||||
@@ -96,12 +98,25 @@ class ChapterHtmlSlimParser {
|
|||||||
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
||||||
int wordsExtractedInBlock = 0;
|
int wordsExtractedInBlock = 0;
|
||||||
|
|
||||||
|
// Resumable parse state. The one-shot parseAndBuildPages() drives these
|
||||||
|
// internally; the incremental section builder drives them across render ticks
|
||||||
|
// so a large single chapter can yield between pages instead of blocking the UI
|
||||||
|
// until the whole thing is laid out. parseFile_ and the expat parser stay alive
|
||||||
|
// for the lifetime of the parse so it can be paused and resumed at buffer
|
||||||
|
// boundaries.
|
||||||
|
XML_Parser xmlParser_ = nullptr;
|
||||||
|
HalFile parseFile_;
|
||||||
|
uint32_t parseStartTime_ = 0;
|
||||||
|
|
||||||
void updateEffectiveInlineStyle();
|
void updateEffectiveInlineStyle();
|
||||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||||
void flushPendingAnchor();
|
void flushPendingAnchor();
|
||||||
void flushPartWordBuffer();
|
void flushPartWordBuffer();
|
||||||
void makePages();
|
void makePages();
|
||||||
|
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
|
||||||
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
|
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||||
|
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||||
|
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
|
||||||
void emitHorizontalRule(const BlockStyle& blockStyle);
|
void emitHorizontalRule(const BlockStyle& blockStyle);
|
||||||
// XML callbacks
|
// XML callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
@@ -141,8 +156,28 @@ class ChapterHtmlSlimParser {
|
|||||||
imageBasePath(imageBasePath),
|
imageBasePath(imageBasePath),
|
||||||
tocAnchors(std::move(tocAnchors)) {}
|
tocAnchors(std::move(tocAnchors)) {}
|
||||||
|
|
||||||
~ChapterHtmlSlimParser() = default;
|
~ChapterHtmlSlimParser();
|
||||||
|
|
||||||
|
// One-shot parse: builds every page before returning (begin + step* + finish).
|
||||||
bool parseAndBuildPages();
|
bool parseAndBuildPages();
|
||||||
|
|
||||||
|
// Resumable parse, for the incremental section builder. Drive as:
|
||||||
|
// if (!beginParse()) fail;
|
||||||
|
// loop: switch (parseStep()) { More: keep going / yield; Done: finishParse(); Error: abortParse(); }
|
||||||
|
// Pages are emitted via completePageFn as they complete during parseStep(), so
|
||||||
|
// the caller can stop once enough pages are built and resume on a later tick.
|
||||||
|
enum class ParseStatus { More, Done, Error };
|
||||||
|
bool beginParse();
|
||||||
|
ParseStatus parseStep();
|
||||||
|
bool finishParse(); // flush the trailing page and tear down; returns true
|
||||||
|
void abortParse(); // tear down without flushing (error / abandon)
|
||||||
|
|
||||||
void addLineToPage(std::shared_ptr<TextBlock> line);
|
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||||
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
|
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
|
||||||
|
|
||||||
|
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
|
||||||
|
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
|
||||||
|
// between beginParse() and finishParse()/abortParse().
|
||||||
|
size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : 0; }
|
||||||
|
size_t parseTotalBytes() { return parseFile_ ? parseFile_.size() : 0; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,8 +137,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort item index for binary search if we have enough items
|
// Sort the (unconditionally-built) item index so every idref lookup uses binary
|
||||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
// search. Without this, small/medium manifests fell back to an O(spine × manifest)
|
||||||
|
// linear rescan of .items.bin per itemref (up to ~200ms/item at large scale).
|
||||||
|
if (!self->itemIndex.empty()) {
|
||||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||||
});
|
});
|
||||||
@@ -284,9 +286,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
// Fallback linear scan, only reached when the index is empty (no manifest
|
||||||
// TODO: This lookup is slow as need to scan through all items each time.
|
// items). The fast binary-search path above is used for all real manifests.
|
||||||
// It can take up to 200ms per item when getting to 1500 items.
|
|
||||||
self->tempItemStore.seek(0);
|
self->tempItemStore.seek(0);
|
||||||
std::string itemId;
|
std::string itemId;
|
||||||
while (self->tempItemStore.available()) {
|
while (self->tempItemStore.available()) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ContentOpfParser final : public Print {
|
|||||||
HalFile tempItemStore;
|
HalFile tempItemStore;
|
||||||
std::string coverItemId;
|
std::string coverItemId;
|
||||||
|
|
||||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
// Index for fast idref→href lookup (binary search over .items.bin)
|
||||||
struct ItemIndexEntry {
|
struct ItemIndexEntry {
|
||||||
uint32_t idHash; // FNV-1a hash of itemId
|
uint32_t idHash; // FNV-1a hash of itemId
|
||||||
uint16_t idLen; // length for collision reduction
|
uint16_t idLen; // length for collision reduction
|
||||||
@@ -41,8 +41,6 @@ class ContentOpfParser final : public Print {
|
|||||||
std::deque<ItemIndexEntry> itemIndex;
|
std::deque<ItemIndexEntry> itemIndex;
|
||||||
bool useItemIndex = false;
|
bool useItemIndex = false;
|
||||||
|
|
||||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
|
||||||
|
|
||||||
// FNV-1a hash function
|
// FNV-1a hash function
|
||||||
static uint32_t fnvHash(const std::string& s) {
|
static uint32_t fnvHash(const std::string& s) {
|
||||||
uint32_t hash = 2166136261u;
|
uint32_t hash = 2166136261u;
|
||||||
|
|||||||
+70
-54
@@ -3,6 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace FsHelpers {
|
namespace FsHelpers {
|
||||||
@@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string normalisePath(const std::string& path) {
|
std::string normalisePath(const std::string& path) {
|
||||||
std::vector<std::string> components;
|
std::vector<std::string_view> components;
|
||||||
std::string component;
|
components.reserve(8); // Eight nested folders is more than we might expect
|
||||||
|
|
||||||
for (const auto c : path) {
|
size_t start = 0;
|
||||||
if (c == '/') {
|
for (size_t i = 0; i <= path.length(); ++i) {
|
||||||
if (!component.empty()) {
|
if (i == path.length() || path[i] == '/') {
|
||||||
|
if (i > start) {
|
||||||
|
std::string_view component(path.data() + start, i - start);
|
||||||
if (component == "..") {
|
if (component == "..") {
|
||||||
if (!components.empty()) {
|
if (!components.empty()) {
|
||||||
components.pop_back();
|
components.pop_back();
|
||||||
@@ -49,28 +52,80 @@ std::string normalisePath(const std::string& path) {
|
|||||||
} else {
|
} else {
|
||||||
components.push_back(component);
|
components.push_back(component);
|
||||||
}
|
}
|
||||||
component.clear();
|
|
||||||
}
|
}
|
||||||
} else {
|
start = i + 1;
|
||||||
component += c;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!component.empty()) {
|
if (components.empty()) {
|
||||||
components.push_back(component);
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total_len = 0;
|
||||||
|
for (const auto& c : components) {
|
||||||
|
total_len += c.length() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string result;
|
std::string result;
|
||||||
for (const auto& c : components) {
|
result.reserve(total_len - 1);
|
||||||
if (!result.empty()) {
|
|
||||||
result += "/";
|
for (size_t i = 0; i < components.size(); ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
result += '/';
|
||||||
}
|
}
|
||||||
result += c;
|
result.append(components[i].data(), components[i].length());
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool naturalLess(const std::string& str1, const std::string& str2) {
|
||||||
|
// Naive natural sort: numeric-aware, case-insensitive
|
||||||
|
const char* s1 = str1.c_str();
|
||||||
|
const char* s2 = str2.c_str();
|
||||||
|
|
||||||
|
// ctype functions require unsigned char values: passing a negative char (UTF-8
|
||||||
|
// bytes above 0x7f with signed char) is undefined behavior
|
||||||
|
const auto isDigit = [](const char c) { return isdigit(static_cast<unsigned char>(c)) != 0; };
|
||||||
|
|
||||||
|
// Iterate while both strings have characters
|
||||||
|
while (*s1 && *s2) {
|
||||||
|
// Check if both are at the start of a number
|
||||||
|
if (isDigit(*s1) && isDigit(*s2)) {
|
||||||
|
// Skip leading zeros and track them
|
||||||
|
while (*s1 == '0') s1++;
|
||||||
|
while (*s2 == '0') s2++;
|
||||||
|
|
||||||
|
// Count digits to compare lengths first
|
||||||
|
int len1 = 0, len2 = 0;
|
||||||
|
while (isDigit(s1[len1])) len1++;
|
||||||
|
while (isDigit(s2[len2])) len2++;
|
||||||
|
|
||||||
|
// Different length so return smaller integer value
|
||||||
|
if (len1 != len2) return len1 < len2;
|
||||||
|
|
||||||
|
// Same length so compare digit by digit
|
||||||
|
for (int i = 0; i < len1; i++) {
|
||||||
|
if (s1[i] != s2[i]) return s1[i] < s2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers equal so advance pointers
|
||||||
|
s1 += len1;
|
||||||
|
s2 += len2;
|
||||||
|
} else {
|
||||||
|
// Regular case-insensitive character comparison
|
||||||
|
const int c1 = tolower(static_cast<unsigned char>(*s1));
|
||||||
|
const int c2 = tolower(static_cast<unsigned char>(*s2));
|
||||||
|
if (c1 != c2) return c1 < c2;
|
||||||
|
s1++;
|
||||||
|
s2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One string is prefix of other
|
||||||
|
return *s1 == '\0' && *s2 != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs) {
|
void sortFileList(std::vector<std::string>& strs) {
|
||||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||||
// Directories first
|
// Directories first
|
||||||
@@ -78,46 +133,7 @@ void sortFileList(std::vector<std::string>& strs) {
|
|||||||
bool isDir2 = str2.back() == '/';
|
bool isDir2 = str2.back() == '/';
|
||||||
if (isDir1 != isDir2) return isDir1;
|
if (isDir1 != isDir2) return isDir1;
|
||||||
|
|
||||||
// Start naive natural sort
|
return naturalLess(str1, str2);
|
||||||
const char* s1 = str1.c_str();
|
|
||||||
const char* s2 = str2.c_str();
|
|
||||||
|
|
||||||
// Iterate while both strings have characters
|
|
||||||
while (*s1 && *s2) {
|
|
||||||
// Check if both are at the start of a number
|
|
||||||
if (isdigit(*s1) && isdigit(*s2)) {
|
|
||||||
// Skip leading zeros and track them
|
|
||||||
while (*s1 == '0') s1++;
|
|
||||||
while (*s2 == '0') s2++;
|
|
||||||
|
|
||||||
// Count digits to compare lengths first
|
|
||||||
int len1 = 0, len2 = 0;
|
|
||||||
while (isdigit(s1[len1])) len1++;
|
|
||||||
while (isdigit(s2[len2])) len2++;
|
|
||||||
|
|
||||||
// Different length so return smaller integer value
|
|
||||||
if (len1 != len2) return len1 < len2;
|
|
||||||
|
|
||||||
// Same length so compare digit by digit
|
|
||||||
for (int i = 0; i < len1; i++) {
|
|
||||||
if (s1[i] != s2[i]) return s1[i] < s2[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numbers equal so advance pointers
|
|
||||||
s1 += len1;
|
|
||||||
s2 += len2;
|
|
||||||
} else {
|
|
||||||
// Regular case-insensitive character comparison
|
|
||||||
char c1 = tolower(*s1);
|
|
||||||
char c2 = tolower(*s2);
|
|
||||||
if (c1 != c2) return c1 < c2;
|
|
||||||
s1++;
|
|
||||||
s2++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One string is prefix of other
|
|
||||||
return *s1 == '\0' && *s2 != '\0';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ std::string decodeUriEscapes(const std::string& path);
|
|||||||
|
|
||||||
std::string normalisePath(const std::string& path);
|
std::string normalisePath(const std::string& path);
|
||||||
|
|
||||||
|
// Numeric-aware, case-insensitive comparison ("2" < "10"). Returns true when str1 orders
|
||||||
|
// before str2. Same ordering sortFileList applies within the file/directory groups.
|
||||||
|
bool naturalLess(const std::string& str1, const std::string& str2);
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs);
|
void sortFileList(std::vector<std::string>& strs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -62,7 +62,14 @@ void FontCacheManager::resetStats() {
|
|||||||
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
|
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
|
||||||
|
|
||||||
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
|
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
|
||||||
scanText_ += text;
|
if (!text) return;
|
||||||
|
const size_t remaining = (scanTextLen_ < SCAN_TEXT_CAPACITY - 1) ? (SCAN_TEXT_CAPACITY - 1 - scanTextLen_) : 0;
|
||||||
|
if (remaining > 0) {
|
||||||
|
const size_t textLen = strnlen(text, remaining);
|
||||||
|
memcpy(scanText_ + scanTextLen_, text, textLen);
|
||||||
|
scanTextLen_ += textLen;
|
||||||
|
scanText_[scanTextLen_] = '\0';
|
||||||
|
}
|
||||||
if (scanFontId_ < 0) scanFontId_ = fontId;
|
if (scanFontId_ < 0) scanFontId_ = fontId;
|
||||||
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
|
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
|
||||||
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
|
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
|
||||||
@@ -80,15 +87,15 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
|
|||||||
manager_->scanMode_ = ScanMode::Scanning;
|
manager_->scanMode_ = ScanMode::Scanning;
|
||||||
manager_->clearCache();
|
manager_->clearCache();
|
||||||
manager_->resetStats();
|
manager_->resetStats();
|
||||||
manager_->scanText_.clear();
|
manager_->scanTextLen_ = 0;
|
||||||
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
|
manager_->scanText_[0] = '\0';
|
||||||
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
|
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
|
||||||
manager_->scanFontId_ = -1;
|
manager_->scanFontId_ = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
||||||
manager_->scanMode_ = ScanMode::None;
|
manager_->scanMode_ = ScanMode::None;
|
||||||
if (manager_->scanText_.empty()) return;
|
if (manager_->scanTextLen_ == 0) return;
|
||||||
|
|
||||||
// Build style bitmask from all styles that appeared during the scan
|
// Build style bitmask from all styles that appeared during the scan
|
||||||
uint8_t styleMask = 0;
|
uint8_t styleMask = 0;
|
||||||
@@ -97,11 +104,10 @@ void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
|||||||
}
|
}
|
||||||
if (styleMask == 0) styleMask = 1; // default to regular
|
if (styleMask == 0) styleMask = 1; // default to regular
|
||||||
|
|
||||||
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
|
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_, styleMask);
|
||||||
|
|
||||||
// Free scan string memory
|
manager_->scanTextLen_ = 0;
|
||||||
manager_->scanText_.clear();
|
manager_->scanText_[0] = '\0';
|
||||||
manager_->scanText_.shrink_to_fit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FontCacheManager::PrewarmScope::~PrewarmScope() {
|
FontCacheManager::PrewarmScope::~PrewarmScope() {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class FontDecompressor;
|
class FontDecompressor;
|
||||||
class SdCardFont;
|
class SdCardFont;
|
||||||
@@ -51,7 +51,9 @@ class FontCacheManager {
|
|||||||
|
|
||||||
enum class ScanMode : uint8_t { None, Scanning };
|
enum class ScanMode : uint8_t { None, Scanning };
|
||||||
ScanMode scanMode_ = ScanMode::None;
|
ScanMode scanMode_ = ScanMode::None;
|
||||||
std::string scanText_;
|
static constexpr size_t SCAN_TEXT_CAPACITY = 2048;
|
||||||
|
char scanText_[SCAN_TEXT_CAPACITY] = {};
|
||||||
|
size_t scanTextLen_ = 0;
|
||||||
uint32_t scanStyleCounts_[4] = {};
|
uint32_t scanStyleCounts_[4] = {};
|
||||||
int scanFontId_ = -1;
|
int scanFontId_ = -1;
|
||||||
};
|
};
|
||||||
|
|||||||
+374
-38
@@ -1,6 +1,7 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
|
#include <BuildScratch.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
||||||
|
|
||||||
|
// Appends the shaped visual form of every RTL token in `text` to `shapedOut`.
|
||||||
|
// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream,
|
||||||
|
// so the SD advance table must be warmed with the presentation forms as well as
|
||||||
|
// the logical codepoints — otherwise every RTL word measurement misses the fast
|
||||||
|
// path and falls through to onGlyphMiss(), which opens the .cpfont and reads
|
||||||
|
// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph.
|
||||||
|
// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so
|
||||||
|
// pure-LTR text pays almost nothing.
|
||||||
|
void appendShapedRtlTokens(const char* text, std::string& shapedOut) {
|
||||||
|
const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; };
|
||||||
|
std::string token;
|
||||||
|
std::string visual;
|
||||||
|
const char* p = text;
|
||||||
|
while (*p) {
|
||||||
|
while (*p && isBreak(*p)) ++p;
|
||||||
|
const char* start = p;
|
||||||
|
bool hasRtlBytes = false;
|
||||||
|
while (*p && !isBreak(*p)) {
|
||||||
|
const auto b = static_cast<unsigned char>(*p);
|
||||||
|
hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB);
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
if (!hasRtlBytes) continue;
|
||||||
|
token.assign(start, p - start);
|
||||||
|
if (BidiUtils::applyBidiVisual(token.c_str(), visual, static_cast<int>(BidiUtils::BidiBaseDir::AUTO))) {
|
||||||
|
shapedOut += visual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
||||||
@@ -57,7 +88,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
|||||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
||||||
auto it = sdCardFonts_.find(fontId);
|
auto it = sdCardFonts_.find(fontId);
|
||||||
if (it != sdCardFonts_.end()) {
|
if (it != sdCardFonts_.end()) {
|
||||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
std::string shaped;
|
||||||
|
appendShapedRtlTokens(utf8Text, shaped);
|
||||||
|
int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
@@ -71,7 +104,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::strin
|
|||||||
// Augment the persistent advance-only table for layout measurement.
|
// Augment the persistent advance-only table for layout measurement.
|
||||||
// The table survives across paragraphs/sections (capped per font), so
|
// The table survives across paragraphs/sections (capped per font), so
|
||||||
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
||||||
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
|
std::string shaped;
|
||||||
|
for (const auto& w : words) {
|
||||||
|
appendShapedRtlTokens(w.c_str(), shaped);
|
||||||
|
}
|
||||||
|
int missed =
|
||||||
|
it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
@@ -91,6 +129,47 @@ void GfxRenderer::begin() {
|
|||||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::releaseFrameBufferForBuild() {
|
||||||
|
// Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so
|
||||||
|
// it cannot move and repeated loans cannot fragment the heap (the previous
|
||||||
|
// free+realloc model measurably decayed the max contiguous block over a
|
||||||
|
// session). The bytes are deposited in the build-scratch registry so
|
||||||
|
// memory-hungry build phases (e.g. InflateStream's tinfl state + window)
|
||||||
|
// can claim them instead of allocating.
|
||||||
|
uint32_t size = 0;
|
||||||
|
uint8_t* scratch = display.lendFrameBufferStorage(&size);
|
||||||
|
frameBuffer = nullptr;
|
||||||
|
if (scratch) {
|
||||||
|
buildscratch::lend(scratch, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GfxRenderer::restoreFrameBufferAfterBuild() {
|
||||||
|
buildscratch::reclaim();
|
||||||
|
display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed
|
||||||
|
frameBuffer = display.getFrameBuffer();
|
||||||
|
return frameBuffer != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) {
|
||||||
|
// Nesting guard: if the framebuffer is already lent out (an outer loan),
|
||||||
|
// stay inert so this end() cannot return storage the outer loan still owns.
|
||||||
|
if (!renderer_.hasFrameBuffer()) return;
|
||||||
|
renderer_.releaseFrameBufferForBuild();
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::FrameBufferLoan::end() {
|
||||||
|
if (!active_) return;
|
||||||
|
active_ = false;
|
||||||
|
if (!renderer_.restoreFrameBufferAfterBuild()) {
|
||||||
|
// Only reachable if the framebuffer never existed, which begin() already
|
||||||
|
// asserts against; kept as a backstop since running blind helps nobody.
|
||||||
|
LOG_ERR("GFX", "Framebuffer restore failed - restarting");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
||||||
|
|
||||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||||
@@ -418,18 +497,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
uint32_t prevCp = 0;
|
uint32_t prevCp = 0;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
||||||
// Skip Hebrew Niqqud (vowel marks)
|
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||||
continue;
|
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||||
}
|
// marks stay centered, raised above the base or (kasra) at their
|
||||||
|
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||||
if (utf8IsCombiningMark(cp)) {
|
// the getGlyph lookup and skip them, as before.
|
||||||
|
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||||
if (!combiningGlyph) continue;
|
if (!combiningGlyph) continue;
|
||||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
const auto anchor = combiningMark::anchorFor(cp);
|
||||||
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left,
|
const int raiseBy =
|
||||||
combiningGlyph->width);
|
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||||
|
const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||||
|
combiningGlyph->left, combiningGlyph->width);
|
||||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -661,8 +743,10 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
if (state) {
|
||||||
drawLine(x, fillY, x + width - 1, fillY, state);
|
fillRectImpl<Color::Black>(x, y, width, height);
|
||||||
|
} else {
|
||||||
|
fillRectImpl<Color::White>(x, y, width, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,26 +778,194 @@ void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) con
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
||||||
if (color == Color::Clear) {
|
switch (color) {
|
||||||
} else if (color == Color::Black) {
|
case Color::Clear:
|
||||||
fillRect(x, y, width, height, true);
|
break;
|
||||||
} else if (color == Color::White) {
|
case Color::Black:
|
||||||
fillRect(x, y, width, height, false);
|
fillRectImpl<Color::Black>(x, y, width, height);
|
||||||
} else if (color == Color::LightGray) {
|
break;
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
case Color::White:
|
||||||
for (int fillX = x; fillX < x + width; fillX++) {
|
fillRectImpl<Color::White>(x, y, width, height);
|
||||||
drawPixelDither<Color::LightGray>(fillX, fillY);
|
break;
|
||||||
|
case Color::LightGray:
|
||||||
|
fillRectImpl<Color::LightGray>(x, y, width, height);
|
||||||
|
break;
|
||||||
|
case Color::DarkGray:
|
||||||
|
fillRectImpl<Color::DarkGray>(x, y, width, height);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <Color C>
|
||||||
|
void GfxRenderer::fillRectImpl(const int x, const int y, const int width, const int height) const {
|
||||||
|
if constexpr (C == Color::Clear) return;
|
||||||
|
if (width <= 0 || height <= 0) return;
|
||||||
|
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||||
|
|
||||||
|
// Clip in logical space.
|
||||||
|
const int screenW = getScreenWidth();
|
||||||
|
const int screenH = getScreenHeight();
|
||||||
|
const int lx0 = std::max(0, x);
|
||||||
|
const int ly0 = std::max(0, y);
|
||||||
|
const int lx1 = std::min(screenW, x + width);
|
||||||
|
const int ly1 = std::min(screenH, y + height);
|
||||||
|
if (lx0 >= lx1 || ly0 >= ly1) return;
|
||||||
|
|
||||||
|
// Rotate the two opposing logical corners into physical-framebuffer space.
|
||||||
|
// The bounding rect in physical space is the rect we need to fill — rotation
|
||||||
|
// is rigid (no shear/stretch) so the bbox of the two corners IS the rect.
|
||||||
|
int paX, paY, pbX, pbY;
|
||||||
|
rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight);
|
||||||
|
rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight);
|
||||||
|
|
||||||
|
const int phyX0 = std::min(paX, pbX);
|
||||||
|
const int phyX1 = std::max(paX, pbX); // inclusive
|
||||||
|
int phyY0 = std::min(paY, pbY);
|
||||||
|
int phyY1 = std::max(paY, pbY);
|
||||||
|
|
||||||
|
// Strip mode: clip Y range to the active band and redirect writes.
|
||||||
|
uint8_t* target = getWriteTarget();
|
||||||
|
const int originY = getWriteOriginY();
|
||||||
|
const int writeRows = getWriteRows();
|
||||||
|
phyY0 = std::max(phyY0, originY);
|
||||||
|
phyY1 = std::min(phyY1, originY + writeRows - 1);
|
||||||
|
if (phyY0 > phyY1) return;
|
||||||
|
|
||||||
|
// Bit/byte layout: MSB-first within a byte, so phyX → bit (7 - (phyX & 7)).
|
||||||
|
// Head and tail masks cover only the in-rect bits of the first/last byte.
|
||||||
|
const int byteStart = phyX0 >> 3;
|
||||||
|
const int byteEnd = phyX1 >> 3; // inclusive
|
||||||
|
const uint8_t headMask = static_cast<uint8_t>(0xFFu >> (phyX0 & 7));
|
||||||
|
const uint8_t tailMask = static_cast<uint8_t>(0xFFu << (7 - (phyX1 & 7)));
|
||||||
|
const int32_t panelStride = static_cast<int32_t>(panelWidthBytes);
|
||||||
|
|
||||||
|
if constexpr (C == Color::Black || C == Color::White) {
|
||||||
|
// Solid fill. Framebuffer: 0 = black, 1 = white.
|
||||||
|
const uint8_t fillByte = (C == Color::Black) ? 0x00u : 0xFFu;
|
||||||
|
for (int py = phyY0; py <= phyY1; ++py) {
|
||||||
|
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
|
||||||
|
if (byteStart == byteEnd) {
|
||||||
|
const uint8_t mask = headMask & tailMask;
|
||||||
|
if constexpr (C == Color::Black) {
|
||||||
|
row[byteStart] &= static_cast<uint8_t>(~mask);
|
||||||
|
} else {
|
||||||
|
row[byteStart] |= mask;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if constexpr (C == Color::Black) {
|
||||||
|
row[byteStart] &= static_cast<uint8_t>(~headMask);
|
||||||
|
if (byteEnd > byteStart + 1) {
|
||||||
|
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
|
||||||
|
}
|
||||||
|
row[byteEnd] &= static_cast<uint8_t>(~tailMask);
|
||||||
|
} else {
|
||||||
|
row[byteStart] |= headMask;
|
||||||
|
if (byteEnd > byteStart + 1) {
|
||||||
|
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
|
||||||
|
}
|
||||||
|
row[byteEnd] |= tailMask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (color == Color::DarkGray) {
|
} else {
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
// Dither (LightGray / DarkGray). Both patterns have period 2 in logical
|
||||||
for (int fillX = x; fillX < x + width; fillX++) {
|
// (x, y), so per physical row we precompute one byte that represents the
|
||||||
drawPixelDither<Color::DarkGray>(fillX, fillY);
|
// pattern across an 8-pixel stretch — every full byte in the row uses
|
||||||
|
// that same value.
|
||||||
|
//
|
||||||
|
// dlxPerPhyX / dlyPerPhyX: how logical (x, y) change as phyX increments
|
||||||
|
// along a physical row. Derived from inverting rotateCoordinates.
|
||||||
|
int dlxPerPhyX = 0, dlyPerPhyX = 0;
|
||||||
|
switch (orientation) {
|
||||||
|
case Portrait:
|
||||||
|
dlxPerPhyX = 0;
|
||||||
|
dlyPerPhyX = 1;
|
||||||
|
break;
|
||||||
|
case PortraitInverted:
|
||||||
|
dlxPerPhyX = 0;
|
||||||
|
dlyPerPhyX = -1;
|
||||||
|
break;
|
||||||
|
case LandscapeClockwise:
|
||||||
|
dlxPerPhyX = -1;
|
||||||
|
dlyPerPhyX = 0;
|
||||||
|
break;
|
||||||
|
case LandscapeCounterClockwise:
|
||||||
|
dlxPerPhyX = 1;
|
||||||
|
dlyPerPhyX = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dither pattern has period 2 in logical space, and each orientation
|
||||||
|
// maps py to logical coords with a fixed parity relationship. The
|
||||||
|
// blackMask byte therefore repeats with period 2 in py. Precompute both
|
||||||
|
// variants outside the row loop to eliminate the per-row switch + 8-bit
|
||||||
|
// construction loop.
|
||||||
|
uint8_t blackMasks[2];
|
||||||
|
for (int parityIdx = 0; parityIdx < 2; ++parityIdx) {
|
||||||
|
const int samplePy = phyY0 + parityIdx;
|
||||||
|
int lxBase = 0, lyBase = 0;
|
||||||
|
switch (orientation) {
|
||||||
|
case Portrait:
|
||||||
|
lxBase = panelHeight - 1 - samplePy;
|
||||||
|
lyBase = byteStart * 8;
|
||||||
|
break;
|
||||||
|
case PortraitInverted:
|
||||||
|
lxBase = samplePy;
|
||||||
|
lyBase = panelWidth - 1 - byteStart * 8;
|
||||||
|
break;
|
||||||
|
case LandscapeClockwise:
|
||||||
|
lxBase = panelWidth - 1 - byteStart * 8;
|
||||||
|
lyBase = panelHeight - 1 - samplePy;
|
||||||
|
break;
|
||||||
|
case LandscapeCounterClockwise:
|
||||||
|
lxBase = byteStart * 8;
|
||||||
|
lyBase = samplePy;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t mask = 0;
|
||||||
|
for (int b = 0; b < 8; ++b) {
|
||||||
|
const int lx = lxBase + b * dlxPerPhyX;
|
||||||
|
const int ly = lyBase + b * dlyPerPhyX;
|
||||||
|
bool isBlack;
|
||||||
|
if constexpr (C == Color::LightGray) {
|
||||||
|
isBlack = ((lx & 1) == 0) && ((ly & 1) == 0);
|
||||||
|
} else { // DarkGray
|
||||||
|
isBlack = (((lx + ly) & 1) == 0);
|
||||||
|
}
|
||||||
|
if (isBlack) mask |= static_cast<uint8_t>(1u << (7 - b));
|
||||||
|
}
|
||||||
|
blackMasks[samplePy & 1] = mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int py = phyY0; py <= phyY1; ++py) {
|
||||||
|
const uint8_t blackMask = blackMasks[py & 1];
|
||||||
|
const uint8_t whiteMask = static_cast<uint8_t>(~blackMask);
|
||||||
|
|
||||||
|
// Dither writes BOTH inks (the slow path called drawPixel for every
|
||||||
|
// pixel — setting or clearing — so we must do the same). Inside the
|
||||||
|
// rect mask: write whiteMask (1s where white, 0s where black). Outside
|
||||||
|
// the rect mask: leave the framebuffer untouched.
|
||||||
|
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
|
||||||
|
if (byteStart == byteEnd) {
|
||||||
|
const uint8_t rectMask = headMask & tailMask;
|
||||||
|
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~rectMask) | (rectMask & whiteMask));
|
||||||
|
} else {
|
||||||
|
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~headMask) | (headMask & whiteMask));
|
||||||
|
if (byteEnd > byteStart + 1) {
|
||||||
|
// Period 2, so every full byte in this row is exactly whiteMask.
|
||||||
|
memset(row + byteStart + 1, whiteMask, byteEnd - byteStart - 1);
|
||||||
|
}
|
||||||
|
row[byteEnd] = static_cast<uint8_t>((row[byteEnd] & ~tailMask) | (tailMask & whiteMask));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::Black>(int, int, int, int) const;
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::White>(int, int, int, int) const;
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::LightGray>(int, int, int, int) const;
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::DarkGray>(int, int, int, int) const;
|
||||||
|
|
||||||
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
|
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
|
||||||
const int radius, const Color color) const {
|
const int radius, const Color color) const {
|
||||||
if (radius <= 0 || color == Color::Clear) {
|
if (radius <= 0 || color == Color::Clear) {
|
||||||
@@ -884,8 +1136,24 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
|
|||||||
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int size) const {
|
||||||
display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width);
|
// Plot the icon pixel-by-pixel through drawPixel (which applies the orientation
|
||||||
|
// transform) instead of the byte-aligned framebuffer blit. The blit snaps the
|
||||||
|
// icon's position to 8px (one byte) along the rotated axis, which prevents it
|
||||||
|
// from aligning with adjacent text; per-pixel plotting is pixel-precise.
|
||||||
|
// Icons are square and 1bpp (MSB-first, bit==0 = ink). The (size-1-row, col)
|
||||||
|
// mapping reproduces the Portrait orientation the blit produced; drawIcon is
|
||||||
|
// only called by the UI themes, which all render in forced Portrait.
|
||||||
|
const int rowBytes = (size + 7) / 8;
|
||||||
|
for (int row = 0; row < size; row++) {
|
||||||
|
for (int col = 0; col < size; col++) {
|
||||||
|
const uint8_t byte = bitmap[row * rowBytes + (col >> 3)];
|
||||||
|
const bool ink = ((byte >> (7 - (col & 7))) & 1) == 0;
|
||||||
|
if (ink) {
|
||||||
|
drawPixel(x + (size - 1 - row), y + col, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
||||||
@@ -1183,6 +1451,20 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
|||||||
display.displayBuffer(refreshMode, fadingFix);
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
|
||||||
|
// The async path has no turn-off-screen hook, which the sunlight fading fix
|
||||||
|
// relies on; keep those users on the blocking path.
|
||||||
|
if (fadingFix) {
|
||||||
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
display.displayBufferAsync(refreshMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
|
||||||
|
|
||||||
|
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
|
||||||
|
|
||||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
if (!text || maxWidth <= 0) return "";
|
if (!text || maxWidth <= 0) return "";
|
||||||
@@ -1429,6 +1711,15 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
||||||
|
// Measure the exact codepoint stream drawText renders: bidi-reordered and
|
||||||
|
// Arabic-shaped (contextual presentation forms, Lam-Alef collapse).
|
||||||
|
// Measuring the raw logical text counts the Alef a ligature absorbs and
|
||||||
|
// uses base-letter advances instead of presentation-form advances, so RTL
|
||||||
|
// lines come out wider than they draw — uneven word gaps and a ragged
|
||||||
|
// right margin.
|
||||||
|
std::string visual;
|
||||||
|
text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO);
|
||||||
|
|
||||||
// Advance table fast-path for SD card fonts during layout.
|
// Advance table fast-path for SD card fonts during layout.
|
||||||
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
||||||
// where kern/lig data was not loaded.
|
// where kern/lig data was not loaded.
|
||||||
@@ -1437,8 +1728,22 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
int32_t widthFP = 0;
|
int32_t widthFP = 0;
|
||||||
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
|
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
|
||||||
const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
|
const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
|
||||||
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const auto& font = fontIt->second;
|
||||||
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
||||||
|
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||||
|
if (BidiUtils::isTransparentMark(cp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
||||||
|
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
||||||
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||||
|
advFP = glyph ? glyph->advanceX : 0;
|
||||||
|
}
|
||||||
widthFP += isSupSub ? (advFP + 1) / 2 : advFP;
|
widthFP += isSupSub ? (advFP + 1) / 2 : advFP;
|
||||||
}
|
}
|
||||||
return fp4::toPixel(widthFP);
|
return fp4::toPixel(widthFP);
|
||||||
@@ -1456,6 +1761,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
|
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||||
|
if (BidiUtils::isTransparentMark(cp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (utf8IsCombiningMark(cp)) {
|
if (utf8IsCombiningMark(cp)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1532,18 +1841,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
uint32_t prevCp = 0;
|
uint32_t prevCp = 0;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
// Skip Hebrew Niqqud (vowel marks)
|
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||||
continue;
|
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||||
}
|
// marks stay centered, raised above the base or (kasra) at their
|
||||||
|
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||||
if (utf8IsCombiningMark(cp)) {
|
// the getGlyph lookup and skip them, as before.
|
||||||
|
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||||
if (!combiningGlyph) continue;
|
if (!combiningGlyph) continue;
|
||||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
const auto anchor = combiningMark::anchorFor(cp);
|
||||||
|
const int raiseBy =
|
||||||
|
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||||
const int combiningX = x - raiseBy;
|
const int combiningX = x - raiseBy;
|
||||||
const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth,
|
const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||||
combiningGlyph->left, combiningGlyph->width);
|
combiningGlyph->left, combiningGlyph->width);
|
||||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||||
continue;
|
continue;
|
||||||
@@ -1577,6 +1889,30 @@ size_t GfxRenderer::getBufferSize() const { return frameBufferSize; }
|
|||||||
// unused
|
// unused
|
||||||
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
||||||
|
|
||||||
|
void GfxRenderer::displayGrayscaleBase(HalDisplay::RefreshMode fallback) const {
|
||||||
|
display.displayGrayscaleBase(fallback, fadingFix);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::preconditionGrayscale() const { display.preconditionGrayscale(); }
|
||||||
|
|
||||||
|
void GfxRenderer::preconditionGrayscale(int x, int y, int w, int h) const {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
// Rotate the logical rect's opposite corners to physical panel coords; the
|
||||||
|
// physical bbox stays axis-aligned for all four orientations.
|
||||||
|
int ax, ay, bx, by;
|
||||||
|
rotateCoordinates(orientation, x, y, &ax, &ay, panelWidth, panelHeight);
|
||||||
|
rotateCoordinates(orientation, x + w - 1, y + h - 1, &bx, &by, panelWidth, panelHeight);
|
||||||
|
int x0 = ax < bx ? ax : bx, x1 = ax > bx ? ax : bx;
|
||||||
|
int y0 = ay < by ? ay : by, y1 = ay > by ? ay : by;
|
||||||
|
if (x0 < 0) x0 = 0;
|
||||||
|
if (y0 < 0) y0 = 0;
|
||||||
|
if (x1 >= panelWidth) x1 = panelWidth - 1;
|
||||||
|
if (y1 >= panelHeight) y1 = panelHeight - 1;
|
||||||
|
if (x1 < x0 || y1 < y0) return;
|
||||||
|
display.preconditionGrayscale(static_cast<uint16_t>(x0), static_cast<uint16_t>(y0),
|
||||||
|
static_cast<uint16_t>(x1 - x0 + 1), static_cast<uint16_t>(y1 - y0 + 1));
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); }
|
void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); }
|
||||||
|
|
||||||
void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); }
|
void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); }
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ class GfxRenderer {
|
|||||||
void drawPixelDither(int x, int y) const;
|
void drawPixelDither(int x, int y) const;
|
||||||
template <Color color>
|
template <Color color>
|
||||||
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
||||||
|
// Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's
|
||||||
|
// two opposing corners into physical-framebuffer space once, then walks each
|
||||||
|
// physical row with head-mask / middle memset / tail-mask byte writes — no
|
||||||
|
// per-pixel rotation, no per-pixel RMW.
|
||||||
|
template <Color color>
|
||||||
|
void fillRectImpl(int x, int y, int width, int height) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GfxRenderer(HalDisplay& halDisplay)
|
explicit GfxRenderer(HalDisplay& halDisplay)
|
||||||
@@ -129,6 +135,17 @@ class GfxRenderer {
|
|||||||
int getScreenWidth() const;
|
int getScreenWidth() const;
|
||||||
int getScreenHeight() const;
|
int getScreenHeight() const;
|
||||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
|
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
|
||||||
|
// grayscale strip rendering) can overlap the panel's refresh time. The
|
||||||
|
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
|
||||||
|
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
|
||||||
|
// support. See HalDisplay::displayBufferAsync for the baseline contract.
|
||||||
|
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
|
void waitRefreshComplete() const;
|
||||||
|
// True when displayBufferAsync() genuinely overlaps: panel defers and
|
||||||
|
// fadingFix isn't forcing the blocking path. Callers can skip overlap
|
||||||
|
// scaffolding (e.g. whole-plane grayscale buffers) when false.
|
||||||
|
bool supportsAsyncRefresh() const;
|
||||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||||
// void displayWindow(int x, int y, int width, int height) const;
|
// void displayWindow(int x, int y, int width, int height) const;
|
||||||
void invertScreen() const;
|
void invertScreen() const;
|
||||||
@@ -177,7 +194,7 @@ class GfxRenderer {
|
|||||||
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
|
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
|
||||||
bool roundBottomLeft, bool roundBottomRight, Color color) const;
|
bool roundBottomLeft, bool roundBottomRight, Color color) const;
|
||||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
void drawIcon(const uint8_t bitmap[], int x, int y, int size) const;
|
||||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||||
float cropY = 0) const;
|
float cropY = 0) const;
|
||||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||||
@@ -218,6 +235,16 @@ class GfxRenderer {
|
|||||||
// Grayscale functions
|
// Grayscale functions
|
||||||
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
||||||
RenderMode getRenderMode() const { return renderMode; }
|
RenderMode getRenderMode() const { return renderMode; }
|
||||||
|
// Grayscale preconditioning settle pass (no-op on X4). The rect overload
|
||||||
|
// takes the gray region in LOGICAL screen coordinates and rotates it to the
|
||||||
|
// panel; the no-arg overload settles the full frame. Call after the BW base
|
||||||
|
// frame is displayed and before the grayscale planes are written.
|
||||||
|
void preconditionGrayscale() const;
|
||||||
|
void preconditionGrayscale(int x, int y, int w, int h) const;
|
||||||
|
// Display the framebuffer as the base frame for a grayscale overlay that
|
||||||
|
// follows (X3: OEM differential base waveform; others: plain display with
|
||||||
|
// `fallback`).
|
||||||
|
void displayGrayscaleBase(HalDisplay::RefreshMode fallback = HalDisplay::HALF_REFRESH) const;
|
||||||
void copyGrayscaleLsbBuffers() const;
|
void copyGrayscaleLsbBuffers() const;
|
||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() const;
|
void displayGrayBuffer() const;
|
||||||
@@ -234,6 +261,34 @@ class GfxRenderer {
|
|||||||
// Font helpers
|
// Font helpers
|
||||||
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
||||||
|
|
||||||
|
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
|
||||||
|
// builds) WITHOUT freeing the allocation, so it never moves and repeated
|
||||||
|
// loans cannot fragment the heap. Between release and restore NOTHING may
|
||||||
|
// draw or display — the panel keeps showing its last refreshed image. The
|
||||||
|
// lent bytes are published via buildscratch::claim() for consumers like
|
||||||
|
// InflateStream. restore returns the buffer white, so the caller must
|
||||||
|
// redraw the full screen; it cannot fail (no allocation involved).
|
||||||
|
void releaseFrameBufferForBuild();
|
||||||
|
bool restoreFrameBufferAfterBuild();
|
||||||
|
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
|
||||||
|
|
||||||
|
// RAII form of the loan above, for blocking build regions with early-return
|
||||||
|
// error paths: restores on scope exit (or explicitly via end()). Display the
|
||||||
|
// popup/screen the panel should hold BEFORE constructing one. Constructing
|
||||||
|
// while the framebuffer is already lent yields an inert loan (nesting-safe).
|
||||||
|
class FrameBufferLoan {
|
||||||
|
public:
|
||||||
|
explicit FrameBufferLoan(GfxRenderer& renderer);
|
||||||
|
~FrameBufferLoan() { end(); }
|
||||||
|
void end();
|
||||||
|
FrameBufferLoan(const FrameBufferLoan&) = delete;
|
||||||
|
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GfxRenderer& renderer_;
|
||||||
|
bool active_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Low level functions
|
// Low level functions
|
||||||
uint8_t* getFrameBuffer() const;
|
uint8_t* getFrameBuffer() const;
|
||||||
size_t getBufferSize() const;
|
size_t getBufferSize() const;
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ STR_ORIENTATION: "Арыентацыя чытання"
|
|||||||
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
|
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
|
||||||
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
|
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
|
||||||
STR_FONT_FAMILY: "Шрыфт чытання"
|
STR_FONT_FAMILY: "Шрыфт чытання"
|
||||||
STR_FONT_SIZE: "Памер шрыфту інтэрфейсу"
|
STR_FONT_SIZE: "Памер шрыфту інтэрфейсу"
|
||||||
STR_LINE_SPACING: "Міжрадковы інтэрвал"
|
STR_LINE_SPACING: "Міжрадковы інтэрвал"
|
||||||
@@ -86,6 +87,7 @@ STR_USERNAME: "Імя карыстальніка"
|
|||||||
STR_PASSWORD: "Пароль"
|
STR_PASSWORD: "Пароль"
|
||||||
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
|
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
|
||||||
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
|
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
|
||||||
|
STR_SEND_METADATA: "Адпраўляць метаданыя дакумента"
|
||||||
STR_AUTHENTICATE: "Аўтарызацыя"
|
STR_AUTHENTICATE: "Аўтарызацыя"
|
||||||
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
|
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
@@ -125,6 +127,7 @@ STR_PAGE_TURN: "Перагортванне"
|
|||||||
STR_PORTRAIT: "Партрэт"
|
STR_PORTRAIT: "Партрэт"
|
||||||
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||||
STR_INVERTED: "Інверсія"
|
STR_INVERTED: "Інверсія"
|
||||||
|
STR_ORIENTATION_INVERTED: "Партрэт 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||||
STR_PREV_NEXT: "Назад/Наперад"
|
STR_PREV_NEXT: "Назад/Наперад"
|
||||||
STR_NEXT_PREV: "Наперад/Назад"
|
STR_NEXT_PREV: "Наперад/Назад"
|
||||||
@@ -175,6 +178,9 @@ STR_EXIT: "« Выхад"
|
|||||||
STR_HOME: "« Галоўная"
|
STR_HOME: "« Галоўная"
|
||||||
STR_SELECT: "Абраць"
|
STR_SELECT: "Абраць"
|
||||||
STR_TOGGLE: "Выбар"
|
STR_TOGGLE: "Выбар"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Пераключыць закладку"
|
||||||
|
STR_BOOKMARK_REMOVED: "Закладка выдалена."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Утрымлівайце Адкрыць, каб выдаліць"
|
||||||
STR_CONFIRM: "Пацв."
|
STR_CONFIRM: "Пацв."
|
||||||
STR_CANCEL: "Адмена"
|
STR_CANCEL: "Адмена"
|
||||||
STR_CONNECT: "Падкл."
|
STR_CONNECT: "Падкл."
|
||||||
@@ -231,7 +237,6 @@ STR_CHAPTER_PREFIX: "Раздзел:"
|
|||||||
STR_PAGES_SEPARATOR: "стар. |"
|
STR_PAGES_SEPARATOR: "стар. |"
|
||||||
STR_BOOK_PREFIX: "Кніга:"
|
STR_BOOK_PREFIX: "Кніга:"
|
||||||
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
|
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
|
||||||
STR_PERCENT_STEP_HINT: "Улева/Управа: 1% Уверх/Уніз: 10%"
|
|
||||||
STR_SYNCING_TIME: "Сінхранізацыя часу..."
|
STR_SYNCING_TIME: "Сінхранізацыя часу..."
|
||||||
STR_CALC_HASH: "Вылічэнне хэша дакумента..."
|
STR_CALC_HASH: "Вылічэнне хэша дакумента..."
|
||||||
STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента"
|
STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента"
|
||||||
@@ -291,7 +296,10 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак"
|
|||||||
STR_LINK: "[спасылка]"
|
STR_LINK: "[спасылка]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
||||||
STR_SLEEP_NEVER: "Ніколі"
|
STR_SLEEP_NEVER: "Ніколі"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Улева/Управа: 1 хв Уверх/Уніз: 5 хв"
|
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
|
||||||
|
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
|
||||||
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
||||||
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
|
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Дадаць схаваную сетку..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Увядзіце назву сеткі (SSID)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
|
|||||||
STR_SLEEPING: "ENTRANT EN REPÒS"
|
STR_SLEEPING: "ENTRANT EN REPÒS"
|
||||||
STR_ENTERING_SLEEP: "Entrant en repòs"
|
STR_ENTERING_SLEEP: "Entrant en repòs"
|
||||||
STR_BROWSE_FILES: "Explora fitxers"
|
STR_BROWSE_FILES: "Explora fitxers"
|
||||||
STR_FILE_TRANSFER: "Transferència"
|
STR_FILE_TRANSFER: "Transferència de fitxers"
|
||||||
STR_SETTINGS_TITLE: "Configuració"
|
STR_SETTINGS_TITLE: "Configuració"
|
||||||
STR_CONTINUE_READING: "Continua llegint"
|
STR_CONTINUE_READING: "Continua llegint"
|
||||||
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
|
|||||||
STR_END_OF_BOOK: "Final del llibre"
|
STR_END_OF_BOOK: "Final del llibre"
|
||||||
STR_EMPTY_CHAPTER: "Capítol buit"
|
STR_EMPTY_CHAPTER: "Capítol buit"
|
||||||
STR_INDEXING: "S'està indexant"
|
STR_INDEXING: "S'està indexant"
|
||||||
|
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
|
||||||
STR_MEMORY_ERROR: "Error de memòria"
|
STR_MEMORY_ERROR: "Error de memòria"
|
||||||
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||||
STR_EMPTY_FILE: "Fitxer buit"
|
STR_EMPTY_FILE: "Fitxer buit"
|
||||||
@@ -28,57 +29,63 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
|||||||
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
||||||
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
||||||
STR_SCANNING: "S'està escanejant..."
|
STR_SCANNING: "S'està escanejant..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "S'estan cercant xarxes Wi-Fi desades..."
|
||||||
STR_CONNECTING: "S'està connectant..."
|
STR_CONNECTING: "S'està connectant..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi desada..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostra"
|
||||||
STR_CONNECTED: "S'ha connectat!"
|
STR_CONNECTED: "S'ha connectat!"
|
||||||
STR_CONNECTION_FAILED: "Error de connexió"
|
STR_CONNECTION_FAILED: "Error de connexió"
|
||||||
STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?"
|
STR_FORGET_NETWORK: "Vols oblidar aquesta xarxa?"
|
||||||
STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?"
|
STR_SAVE_PASSWORD: "Vols desar la contrasenya per a la propera vegada?"
|
||||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
||||||
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
||||||
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||||
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
||||||
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
||||||
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a aquesta xarxa Wi-Fi"
|
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a aquesta xarxa Wi-Fi"
|
||||||
STR_OPEN_URL_HINT: "Obriu aquest URL al navegador"
|
STR_OPEN_URL_HINT: "Obre aquest URL al navegador"
|
||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
||||||
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
|
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
|
||||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||||
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Introdueix la contrasenya Wi-Fi"
|
||||||
STR_TO_PREFIX: "a "
|
STR_TO_PREFIX: "a "
|
||||||
STR_CALIBRE_RECEIVING: "S'està rebent: "
|
STR_CALIBRE_RECEIVING: "S'està rebent: "
|
||||||
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
||||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu aquesta pantalla oberta mentre s'envia\""
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantén aquesta pantalla oberta mentre s'envia\""
|
||||||
STR_CAT_DISPLAY: "Visualització"
|
STR_CAT_DISPLAY: "Visualització"
|
||||||
STR_CAT_READER: "Lector"
|
STR_CAT_READER: "Lector"
|
||||||
STR_CAT_CONTROLS: "Controls"
|
STR_CAT_CONTROLS: "Controls"
|
||||||
STR_CAT_SYSTEM: "Sistema"
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
||||||
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
|
STR_SLEEP_COVER_MODE: "Ajust de la portada en repòs"
|
||||||
STR_HIDE_BATTERY: "Oculta el % de bateria"
|
STR_HIDE_BATTERY: "Oculta el % de bateria"
|
||||||
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
|
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
|
||||||
STR_TEXT_AA: "Antialiàsing del text"
|
STR_TEXT_AA: "Antialiàsing del text"
|
||||||
STR_IMAGES: "Imatges"
|
STR_IMAGES: "Imatges"
|
||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||||
STR_IMAGES_SUPPRESS: "Suprimir"
|
STR_IMAGES_SUPPRESS: "Suprimir"
|
||||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
STR_EOB_HOME: "Inici"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continua amb"
|
||||||
|
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
|
||||||
STR_ORIENTATION: "Orientació de lectura"
|
STR_ORIENTATION: "Orientació de lectura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
STR_LONG_PRESS_BEHAVIOR: "Acció en mantenir premut un botó"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||||
STR_FONT_FAMILY: "Tipus de lletra"
|
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||||
STR_FONT_SIZE: "Mida de la lletra (UI)"
|
STR_FONT_FAMILY: "Font del lector"
|
||||||
|
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||||
STR_LINE_SPACING: "Interlineat del lector"
|
STR_LINE_SPACING: "Interlineat del lector"
|
||||||
STR_SCREEN_MARGIN: "Marge de pantalla del lector"
|
STR_SCREEN_MARGIN: "Marge de pantalla del lector"
|
||||||
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
||||||
@@ -87,15 +94,16 @@ STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
|
|||||||
STR_SHOW_HIDDEN_FILES: "Mostra fitxers ocults"
|
STR_SHOW_HIDDEN_FILES: "Mostra fitxers ocults"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
||||||
STR_REFRESH_FREQ: "Freqüència de refresc"
|
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||||
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
||||||
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
|
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
|
||||||
STR_LANGUAGE: "Idioma"
|
STR_LANGUAGE: "Llengua"
|
||||||
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
||||||
STR_USERNAME: "Nom d'usuari"
|
STR_USERNAME: "Nom d'usuari"
|
||||||
STR_PASSWORD: "Contrasenya"
|
STR_PASSWORD: "Contrasenya"
|
||||||
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
||||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||||
|
STR_SEND_METADATA: "Envia metadades del document"
|
||||||
STR_AUTHENTICATE: "Autentica"
|
STR_AUTHENTICATE: "Autentica"
|
||||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||||
@@ -135,10 +143,13 @@ STR_PAGE_TURN: "Canvi de pàgina"
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Horitzontal horari"
|
STR_LANDSCAPE_CW: "Horitzontal horari"
|
||||||
STR_INVERTED: "Invertit"
|
STR_INVERTED: "Invertit"
|
||||||
|
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||||
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
||||||
STR_PREV_NEXT: "Anterior/Següent"
|
STR_PREV_NEXT: "Anterior/Següent"
|
||||||
STR_NEXT_PREV: "Següent/Anterior"
|
STR_NEXT_PREV: "Següent/Anterior"
|
||||||
STR_DISABLED: "Desactivats"
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||||
|
STR_DISABLED: "Sense funció"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Petita"
|
STR_SMALL: "Petita"
|
||||||
@@ -166,17 +177,16 @@ STR_UPDATING: "S'està actualitzant..."
|
|||||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
||||||
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
||||||
STR_UPDATE_COMPLETE: "Actualització completada"
|
STR_UPDATE_COMPLETE: "Actualització completada"
|
||||||
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar"
|
STR_POWER_ON_HINT: "Prem i mantén premut el botó d'encesa per tornar a engegar"
|
||||||
STR_NO_ENTRIES: "No s'ha trobat cap entrada"
|
STR_NO_ENTRIES: "No s'ha trobat cap entrada"
|
||||||
STR_DOWNLOADING: "S'està baixant..."
|
STR_DOWNLOADING: "S'està baixant..."
|
||||||
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
||||||
STR_ERROR_MSG: "Error:"
|
STR_ERROR_MSG: "Error:"
|
||||||
STR_UNNAMED: "Sense nom"
|
STR_UNNAMED: "Sense nom"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar"
|
STR_HOLD_OPEN_TO_DELETE: "Mantén premut Obre per esborrar"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre."
|
|
||||||
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
||||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||||
STR_NETWORK_PREFIX: "Xarxa: "
|
STR_NETWORK_PREFIX: "Xarxa: "
|
||||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||||
@@ -189,6 +199,7 @@ STR_HOME: "« Inici"
|
|||||||
STR_SELECT: "Selecciona"
|
STR_SELECT: "Selecciona"
|
||||||
STR_SELECTED: "Seleccionat"
|
STR_SELECTED: "Seleccionat"
|
||||||
STR_TOGGLE: "Canvia"
|
STR_TOGGLE: "Canvia"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Afegeix o elimina el punt de llibre"
|
||||||
STR_CONFIRM: "Confirma"
|
STR_CONFIRM: "Confirma"
|
||||||
STR_CANCEL: "Cancel·la"
|
STR_CANCEL: "Cancel·la"
|
||||||
STR_CONNECT: "Connecta"
|
STR_CONNECT: "Connecta"
|
||||||
@@ -207,7 +218,7 @@ STR_DIR_RIGHT: "Dreta"
|
|||||||
STR_DIR_UP: "Amunt"
|
STR_DIR_UP: "Amunt"
|
||||||
STR_DIR_DOWN: "Avall"
|
STR_DIR_DOWN: "Avall"
|
||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
|
STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
|
||||||
STR_FILTER_CONTRAST: "Contrast"
|
STR_FILTER_CONTRAST: "Contrast"
|
||||||
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||||
@@ -229,25 +240,26 @@ STR_THEME_CLASSIC: "Clàssic"
|
|||||||
STR_THEME_LYRA: "Lyra"
|
STR_THEME_LYRA: "Lyra"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||||
STR_BOOKMARKS: "Punts de llibre"
|
STR_BOOKMARKS: "Punts de llibre"
|
||||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||||
|
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
|
||||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||||
STR_QUICK_RESUME: "Represa ràpida"
|
STR_QUICK_RESUME: "Represa ràpida"
|
||||||
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||||
STR_REMOVE_FROM_RECENTS: "Voleu suprimir-lo de Llibres recents?"
|
STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?"
|
||||||
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
||||||
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
|
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
|
||||||
STR_FORGET_AND_REMOVE: "Voleu suprimir la contrasenya desada?"
|
STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i suprimir la contrasenya desada?"
|
||||||
STR_FORGET_BUTTON: "Oblida"
|
STR_FORGET_BUTTON: "Oblida"
|
||||||
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||||
STR_CALIBRE_SETUP: "Configura"
|
STR_CALIBRE_SETUP: "Configura"
|
||||||
STR_CALIBRE_STATUS: "Estat"
|
STR_CALIBRE_STATUS: "Estat"
|
||||||
STR_CLEAR_BUTTON: "Esborra"
|
STR_CLEAR_BUTTON: "Esborra"
|
||||||
STR_DEFAULT_VALUE: "Per defecte"
|
STR_DEFAULT_VALUE: "Per defecte"
|
||||||
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol"
|
STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
|
||||||
STR_UNASSIGNED: "No assignat"
|
STR_UNASSIGNED: "No assignat"
|
||||||
STR_ALREADY_ASSIGNED: "Ja assignat"
|
STR_ALREADY_ASSIGNED: "Ja assignat"
|
||||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
||||||
@@ -261,20 +273,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
|
|||||||
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
||||||
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
||||||
STR_DELETE: "Esborra"
|
STR_DELETE: "Esborra"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar aquest punt de llibre?"
|
STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar aquest punt de llibre?"
|
||||||
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
||||||
STR_CHAPTER_PREFIX: "Capítol: "
|
STR_CHAPTER_PREFIX: "Capítol: "
|
||||||
STR_PAGES_SEPARATOR: " pàgines | "
|
STR_PAGES_SEPARATOR: " pàgines | "
|
||||||
STR_BOOK_PREFIX: "Llibre: "
|
STR_BOOK_PREFIX: "Llibre: "
|
||||||
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
|
STR_CALIBRE_URL_HINT: "Per al Calibre, afegeix /opds a la URL"
|
||||||
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
|
|
||||||
STR_SYNCING_TIME: "S'està sincronitzant el temps..."
|
STR_SYNCING_TIME: "S'està sincronitzant el temps..."
|
||||||
STR_CALC_HASH: "S'està calculant el hash del document..."
|
STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
|
||||||
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
|
STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
|
||||||
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
||||||
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||||
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
||||||
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader a la configuració"
|
STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader a la configuració"
|
||||||
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
||||||
STR_REMOTE_LABEL: "Remot:"
|
STR_REMOTE_LABEL: "Remot:"
|
||||||
STR_LOCAL_LABEL: "Local:"
|
STR_LOCAL_LABEL: "Local:"
|
||||||
@@ -284,7 +295,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
|||||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||||
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
||||||
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?"
|
STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
|
||||||
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
||||||
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
|
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
|
||||||
@@ -294,18 +305,20 @@ STR_BOOK_S_STYLE: "Estil del llibre"
|
|||||||
STR_EMBEDDED_STYLE: "Estil incrustat"
|
STR_EMBEDDED_STYLE: "Estil incrustat"
|
||||||
STR_FOCUS_READING: "Lectura enfocada"
|
STR_FOCUS_READING: "Lectura enfocada"
|
||||||
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorn ràpid des de les notes al peu"
|
||||||
STR_FOOTNOTES: "Notes al peu"
|
STR_FOOTNOTES: "Notes al peu"
|
||||||
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
|
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
|
||||||
STR_LINK: "[enllaç]"
|
STR_LINK: "[enllaç]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Mai"
|
STR_SLEEP_NEVER: "Mai"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min"
|
STR_STEP_HINT_FRONT: "Botons frontals:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botons laterals:"
|
||||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
|
||||||
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
||||||
STR_FORCE_REFRESH: "Refresca la pantalla"
|
STR_FORCE_REFRESH: "Refresca la pantalla"
|
||||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons."
|
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantén premut el botó d'encesa durant uns segons."
|
||||||
STR_NEXT_PAGE: "Pàgina següent »"
|
STR_NEXT_PAGE: "Pàgina següent »"
|
||||||
STR_PREV_PAGE: "« Pàgina anterior"
|
STR_PREV_PAGE: "« Pàgina anterior"
|
||||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||||
@@ -334,19 +347,20 @@ STR_SERVER_NAME: "Nom del servidor"
|
|||||||
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
|
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
|
||||||
STR_DELETE_SERVER: "Suprimeix el servidor"
|
STR_DELETE_SERVER: "Suprimeix el servidor"
|
||||||
STR_OPDS_SERVERS: "Servidors OPDS"
|
STR_OPDS_SERVERS: "Servidors OPDS"
|
||||||
STR_MANAGE_FONTS: "Gestiona els tipus de lletra"
|
STR_MANAGE_FONTS: "Gestiona les fonts"
|
||||||
STR_FONT_BROWSER: "Navegador de tipus de lletra"
|
STR_FONT_BROWSER: "Navegador de fonts"
|
||||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..."
|
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
|
||||||
STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles"
|
STR_NO_FONTS_AVAILABLE: "No hi ha fonts disponibles"
|
||||||
STR_FONT_INSTALLED: "Tipus de lletra instal·lat!"
|
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
||||||
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra"
|
STR_FONT_INSTALLED: "Font instal·lada!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació de la font"
|
||||||
STR_INSTALLED: "Instal·lat"
|
STR_INSTALLED: "Instal·lat"
|
||||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||||
STR_CRASH_TITLE: "Bloqueig del sistema"
|
STR_CRASH_TITLE: "Fallada del sistema"
|
||||||
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Incloeu aquest fitxer a l'informe d'errors."
|
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Inclou aquest fitxer a l'informe d'errors."
|
||||||
STR_CRASH_REASON: "Motiu del bloqueig:"
|
STR_CRASH_REASON: "Motiu de la fallada"
|
||||||
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
||||||
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
||||||
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
||||||
@@ -359,20 +373,22 @@ STR_KB_TIPS: "Consells:"
|
|||||||
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
||||||
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL"
|
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL"
|
||||||
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
||||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT: caràcter secundari"
|
||||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari"
|
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT: majúscules o caràcter secundari"
|
||||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari"
|
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT: minúscules o caràcter secundari"
|
||||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||||
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
||||||
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un fitxer de firmware (.bin)"
|
STR_SELECT_FIRMWARE_FILE: "Selecciona un fitxer de firmware (.bin)"
|
||||||
STR_NO_BIN_FILES: "No s'han trobat fitxers .bin"
|
STR_NO_BIN_FILES: "No s'han trobat fitxers .bin"
|
||||||
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||||
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
||||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||||
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
||||||
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?"
|
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
||||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apaguis el dispositiu!"
|
||||||
STR_RECOVERY_MODE: "Mode de recuperació"
|
STR_RECOVERY_MODE: "Mode de recuperació"
|
||||||
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo"
|
STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Afegeix una xarxa oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introdueix el nom de la xarxa (SSID)"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
|||||||
STR_MAC_ADDRESS: "MAC adresa:"
|
STR_MAC_ADDRESS: "MAC adresa:"
|
||||||
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
|
||||||
STR_TO_PREFIX: "pro"
|
STR_TO_PREFIX: "k "
|
||||||
STR_CALIBRE_RECEIVING: "Příjem:"
|
STR_CALIBRE_RECEIVING: "Příjem:"
|
||||||
STR_CALIBRE_RECEIVED: "Přijato:"
|
STR_CALIBRE_RECEIVED: "Přijato:"
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
||||||
@@ -58,7 +58,7 @@ STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
|
|||||||
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
|
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
|
||||||
STR_CAT_DISPLAY: "Displej"
|
STR_CAT_DISPLAY: "Displej"
|
||||||
STR_CAT_READER: "Čtečka"
|
STR_CAT_READER: "Čtečka"
|
||||||
STR_CAT_CONTROLS: "Ovládací prvky"
|
STR_CAT_CONTROLS: "Ovládání"
|
||||||
STR_CAT_SYSTEM: "Systém"
|
STR_CAT_SYSTEM: "Systém"
|
||||||
STR_SLEEP_SCREEN: "Obrazovka spánku"
|
STR_SLEEP_SCREEN: "Obrazovka spánku"
|
||||||
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
|
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
|
||||||
@@ -73,6 +73,7 @@ STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Příliš žluťoučký kůň úpěl ďábelské ódy"
|
||||||
STR_FONT_FAMILY: "Rodina písem čtečky"
|
STR_FONT_FAMILY: "Rodina písem čtečky"
|
||||||
STR_FONT_SIZE: "Velikost písma rozhraní"
|
STR_FONT_SIZE: "Velikost písma rozhraní"
|
||||||
STR_LINE_SPACING: "Řádkování čtečky"
|
STR_LINE_SPACING: "Řádkování čtečky"
|
||||||
@@ -91,6 +92,7 @@ STR_USERNAME: "Uživatelské jméno"
|
|||||||
STR_PASSWORD: "Heslo"
|
STR_PASSWORD: "Heslo"
|
||||||
STR_SYNC_SERVER_URL: "URL synch. serveru"
|
STR_SYNC_SERVER_URL: "URL synch. serveru"
|
||||||
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
||||||
|
STR_SEND_METADATA: "Odesílat metadata dokumentu"
|
||||||
STR_AUTHENTICATE: "Ověření"
|
STR_AUTHENTICATE: "Ověření"
|
||||||
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
||||||
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
||||||
@@ -130,6 +132,7 @@ STR_PAGE_TURN: "Otáčení stránek"
|
|||||||
STR_PORTRAIT: "Na výšku"
|
STR_PORTRAIT: "Na výšku"
|
||||||
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
||||||
STR_INVERTED: "Invertovaný"
|
STR_INVERTED: "Invertovaný"
|
||||||
|
STR_ORIENTATION_INVERTED: "Na výšku 180°"
|
||||||
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
||||||
STR_PREV_NEXT: "Předchozí/Další"
|
STR_PREV_NEXT: "Předchozí/Další"
|
||||||
STR_NEXT_PREV: "Další/Předchozí"
|
STR_NEXT_PREV: "Další/Předchozí"
|
||||||
@@ -180,6 +183,9 @@ STR_EXIT: "« Konec"
|
|||||||
STR_HOME: "« Domů"
|
STR_HOME: "« Domů"
|
||||||
STR_SELECT: "Vybrat"
|
STR_SELECT: "Vybrat"
|
||||||
STR_TOGGLE: "Přepnout"
|
STR_TOGGLE: "Přepnout"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Přepnout záložku"
|
||||||
|
STR_BOOKMARK_REMOVED: "Záložka odstraněna."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Podržte Otevřít pro smazání"
|
||||||
STR_CONFIRM: "Potvrdit"
|
STR_CONFIRM: "Potvrdit"
|
||||||
STR_CANCEL: "Zrušit"
|
STR_CANCEL: "Zrušit"
|
||||||
STR_CONNECT: "Připojit"
|
STR_CONNECT: "Připojit"
|
||||||
@@ -238,7 +244,6 @@ STR_CHAPTER_PREFIX: "Kapitola:"
|
|||||||
STR_PAGES_SEPARATOR: "stránek |"
|
STR_PAGES_SEPARATOR: "stránek |"
|
||||||
STR_BOOK_PREFIX: "Kniha:"
|
STR_BOOK_PREFIX: "Kniha:"
|
||||||
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
|
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
|
||||||
STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %"
|
|
||||||
STR_SYNCING_TIME: "Čas synchronizace..."
|
STR_SYNCING_TIME: "Čas synchronizace..."
|
||||||
STR_CALC_HASH: "Výpočet hashe dokumentu..."
|
STR_CALC_HASH: "Výpočet hashe dokumentu..."
|
||||||
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
|
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
|
||||||
@@ -268,5 +273,8 @@ STR_OPDS_SERVER_URL: "URL serveru OPDS"
|
|||||||
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
|
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikdy"
|
STR_SLEEP_NEVER: "Nikdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vlevo/Vpravo: 1 min Nahoru/Dolů: 5 min"
|
STR_STEP_HINT_FRONT: "Přední tlačítka:"
|
||||||
|
STR_STEP_HINT_SIDE: "Boční tlačítka:"
|
||||||
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
|
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Přidat skrytou síť..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Zadejte název sítě (SSID)"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Høj bly gom vandt fræk sexquiz på wc"
|
||||||
STR_FONT_FAMILY: "Læser skrifttype"
|
STR_FONT_FAMILY: "Læser skrifttype"
|
||||||
STR_FONT_SIZE: "Læser skriftstørrelse"
|
STR_FONT_SIZE: "Læser skriftstørrelse"
|
||||||
STR_LINE_SPACING: "Linjeafstand"
|
STR_LINE_SPACING: "Linjeafstand"
|
||||||
@@ -96,6 +97,7 @@ STR_USERNAME: "Brugernavn"
|
|||||||
STR_PASSWORD: "Adgangskode"
|
STR_PASSWORD: "Adgangskode"
|
||||||
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
|
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
|
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
|
||||||
|
STR_SEND_METADATA: "Send dokumentmetadata"
|
||||||
STR_AUTHENTICATE: "Godkend"
|
STR_AUTHENTICATE: "Godkend"
|
||||||
STR_KOREADER_USERNAME: "KOReader brugernavn"
|
STR_KOREADER_USERNAME: "KOReader brugernavn"
|
||||||
STR_KOREADER_PASSWORD: "KOReader adgangskode"
|
STR_KOREADER_PASSWORD: "KOReader adgangskode"
|
||||||
@@ -135,6 +137,7 @@ STR_PAGE_TURN: "Sideskift"
|
|||||||
STR_PORTRAIT: "Portræt"
|
STR_PORTRAIT: "Portræt"
|
||||||
STR_LANDSCAPE_CW: "Liggende med uret"
|
STR_LANDSCAPE_CW: "Liggende med uret"
|
||||||
STR_INVERTED: "Inverteret"
|
STR_INVERTED: "Inverteret"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portræt 180°"
|
||||||
STR_LANDSCAPE_CCW: "Liggende mod uret"
|
STR_LANDSCAPE_CCW: "Liggende mod uret"
|
||||||
STR_PREV_NEXT: "Forrige/Næste"
|
STR_PREV_NEXT: "Forrige/Næste"
|
||||||
STR_NEXT_PREV: "Næste/Forrige"
|
STR_NEXT_PREV: "Næste/Forrige"
|
||||||
@@ -186,6 +189,9 @@ STR_HOME: "« Hjem"
|
|||||||
STR_SELECT: "Vælg"
|
STR_SELECT: "Vælg"
|
||||||
STR_SELECTED: "Valgt"
|
STR_SELECTED: "Valgt"
|
||||||
STR_TOGGLE: "Skift"
|
STR_TOGGLE: "Skift"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Skift bogmærke"
|
||||||
|
STR_BOOKMARK_REMOVED: "Bogmærke fjernet."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Hold Åbn nede for at slette"
|
||||||
STR_CONFIRM: "Bekræft"
|
STR_CONFIRM: "Bekræft"
|
||||||
STR_CANCEL: "Annuller"
|
STR_CANCEL: "Annuller"
|
||||||
STR_CONNECT: "Forbind"
|
STR_CONNECT: "Forbind"
|
||||||
@@ -261,7 +267,6 @@ STR_CHAPTER_PREFIX: "Kapitel: "
|
|||||||
STR_PAGES_SEPARATOR: " sider | "
|
STR_PAGES_SEPARATOR: " sider | "
|
||||||
STR_BOOK_PREFIX: "Bog: "
|
STR_BOOK_PREFIX: "Bog: "
|
||||||
STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre"
|
STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre"
|
||||||
STR_PERCENT_STEP_HINT: "Venstre/Højre: 1% Op/Ned: 10%"
|
|
||||||
STR_SYNCING_TIME: "Synkroniserer tid..."
|
STR_SYNCING_TIME: "Synkroniserer tid..."
|
||||||
STR_CALC_HASH: "Beregner dokument-hash..."
|
STR_CALC_HASH: "Beregner dokument-hash..."
|
||||||
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
||||||
@@ -293,8 +298,11 @@ STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Aldrig"
|
STR_SLEEP_NEVER: "Aldrig"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Højre: 1 min Op/Ned: 5 min"
|
STR_STEP_HINT_FRONT: "Frontknapper:"
|
||||||
|
STR_STEP_HINT_SIDE: "Sideknapper:"
|
||||||
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
|
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
|
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
|
||||||
STR_TILT_PAGE_TURN: "Vip for at vende side"
|
STR_TILT_PAGE_TURN: "Vip for at vende side"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Tilføj skjult netværk..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Indtast netværksnavn (SSID)"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Pa's wijze lynx bezag vroom het fikse aquaduct"
|
||||||
STR_FONT_FAMILY: "Lettertype lezer"
|
STR_FONT_FAMILY: "Lettertype lezer"
|
||||||
STR_FONT_SIZE: "Lettergrootte lezer"
|
STR_FONT_SIZE: "Lettergrootte lezer"
|
||||||
STR_LINE_SPACING: "Regelafstand lezer"
|
STR_LINE_SPACING: "Regelafstand lezer"
|
||||||
@@ -96,6 +97,7 @@ STR_USERNAME: "Gebruikersnaam"
|
|||||||
STR_PASSWORD: "Wachtwoord"
|
STR_PASSWORD: "Wachtwoord"
|
||||||
STR_SYNC_SERVER_URL: "Sync-server URL"
|
STR_SYNC_SERVER_URL: "Sync-server URL"
|
||||||
STR_DOCUMENT_MATCHING: "Documentkoppeling"
|
STR_DOCUMENT_MATCHING: "Documentkoppeling"
|
||||||
|
STR_SEND_METADATA: "Documentmetadata versturen"
|
||||||
STR_AUTHENTICATE: "Authenticatie"
|
STR_AUTHENTICATE: "Authenticatie"
|
||||||
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
|
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
|
||||||
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
|
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
|
||||||
@@ -134,7 +136,8 @@ STR_SLEEP: "Slaap"
|
|||||||
STR_PAGE_TURN: "Pagina omslaan"
|
STR_PAGE_TURN: "Pagina omslaan"
|
||||||
STR_PORTRAIT: "Staand"
|
STR_PORTRAIT: "Staand"
|
||||||
STR_LANDSCAPE_CW: "Liggend (rechtsom)"
|
STR_LANDSCAPE_CW: "Liggend (rechtsom)"
|
||||||
STR_INVERTED: "Omgekeerd"
|
STR_INVERTED: "Geïnverteerd"
|
||||||
|
STR_ORIENTATION_INVERTED: "Staand 180°"
|
||||||
STR_LANDSCAPE_CCW: "Liggend (linksom)"
|
STR_LANDSCAPE_CCW: "Liggend (linksom)"
|
||||||
STR_PREV_NEXT: "Vorige/Volgende"
|
STR_PREV_NEXT: "Vorige/Volgende"
|
||||||
STR_NEXT_PREV: "Volgende/Vorige"
|
STR_NEXT_PREV: "Volgende/Vorige"
|
||||||
@@ -186,6 +189,9 @@ STR_HOME: "« Home"
|
|||||||
STR_SELECT: "Kies"
|
STR_SELECT: "Kies"
|
||||||
STR_SELECTED: "Geselecteerd"
|
STR_SELECTED: "Geselecteerd"
|
||||||
STR_TOGGLE: "Wissel"
|
STR_TOGGLE: "Wissel"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Bladwijzer wisselen"
|
||||||
|
STR_BOOKMARK_REMOVED: "Bladwijzer verwijderd."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Houd Openen ingedrukt om te verwijderen"
|
||||||
STR_CONFIRM: "Bevestig"
|
STR_CONFIRM: "Bevestig"
|
||||||
STR_CANCEL: "Annuleer"
|
STR_CANCEL: "Annuleer"
|
||||||
STR_CONNECT: "Verbind"
|
STR_CONNECT: "Verbind"
|
||||||
@@ -261,7 +267,6 @@ STR_CHAPTER_PREFIX: "Hoofdstuk: "
|
|||||||
STR_PAGES_SEPARATOR: " pagina's | "
|
STR_PAGES_SEPARATOR: " pagina's | "
|
||||||
STR_BOOK_PREFIX: "Boek: "
|
STR_BOOK_PREFIX: "Boek: "
|
||||||
STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL"
|
STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL"
|
||||||
STR_PERCENT_STEP_HINT: "Links/Rechts: 1% Omhoog/Omlaag: 10%"
|
|
||||||
STR_SYNCING_TIME: "Tijd synchroniseren..."
|
STR_SYNCING_TIME: "Tijd synchroniseren..."
|
||||||
STR_CALC_HASH: "Document-hash berekenen..."
|
STR_CALC_HASH: "Document-hash berekenen..."
|
||||||
STR_HASH_FAILED: "Document-hash berekenen mislukt"
|
STR_HASH_FAILED: "Document-hash berekenen mislukt"
|
||||||
@@ -293,8 +298,11 @@ STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nooit"
|
STR_SLEEP_NEVER: "Nooit"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 min Omhoog/Omlaag: 5 min"
|
STR_STEP_HINT_FRONT: "Voorknoppen:"
|
||||||
|
STR_STEP_HINT_SIDE: "Zijknoppen:"
|
||||||
STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
||||||
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
|
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Verborgen netwerk toevoegen..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Voer netwerknaam in (SSID)"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "No chapters"
|
|||||||
STR_END_OF_BOOK: "End of book"
|
STR_END_OF_BOOK: "End of book"
|
||||||
STR_EMPTY_CHAPTER: "Empty chapter"
|
STR_EMPTY_CHAPTER: "Empty chapter"
|
||||||
STR_INDEXING: "Indexing"
|
STR_INDEXING: "Indexing"
|
||||||
|
STR_INDEX_FAILED: "Failed to index - invalid book"
|
||||||
STR_MEMORY_ERROR: "Memory error"
|
STR_MEMORY_ERROR: "Memory error"
|
||||||
STR_PAGE_LOAD_ERROR: "Page load error"
|
STR_PAGE_LOAD_ERROR: "Page load error"
|
||||||
STR_EMPTY_FILE: "Empty file"
|
STR_EMPTY_FILE: "Empty file"
|
||||||
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Wi-Fi Networks"
|
|||||||
STR_NO_NETWORKS: "No networks found"
|
STR_NO_NETWORKS: "No networks found"
|
||||||
STR_NETWORKS_FOUND: "%zu networks found"
|
STR_NETWORKS_FOUND: "%zu networks found"
|
||||||
STR_SCANNING: "Scanning..."
|
STR_SCANNING: "Scanning..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..."
|
||||||
STR_CONNECTING: "Connecting..."
|
STR_CONNECTING: "Connecting..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..."
|
||||||
|
STR_SHOW_NETWORKS: "Show"
|
||||||
STR_CONNECTED: "Connected!"
|
STR_CONNECTED: "Connected!"
|
||||||
STR_CONNECTION_FAILED: "Connection Failed"
|
STR_CONNECTION_FAILED: "Connection Failed"
|
||||||
STR_FORGET_NETWORK: "Forget Network?"
|
STR_FORGET_NETWORK: "Forget Network?"
|
||||||
@@ -49,6 +53,8 @@ STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
|||||||
STR_MAC_ADDRESS: "MAC address:"
|
STR_MAC_ADDRESS: "MAC address:"
|
||||||
STR_CHECKING_WIFI: "Checking Wi-Fi..."
|
STR_CHECKING_WIFI: "Checking Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
|
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Add hidden network..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Enter network name (SSID)"
|
||||||
STR_TO_PREFIX: "to "
|
STR_TO_PREFIX: "to "
|
||||||
STR_CALIBRE_RECEIVING: "Receiving: "
|
STR_CALIBRE_RECEIVING: "Receiving: "
|
||||||
STR_CALIBRE_RECEIVED: "Received: "
|
STR_CALIBRE_RECEIVED: "Received: "
|
||||||
@@ -70,6 +76,8 @@ STR_IMAGES: "Images"
|
|||||||
STR_IMAGES_DISPLAY: "Display"
|
STR_IMAGES_DISPLAY: "Display"
|
||||||
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
||||||
STR_IMAGES_SUPPRESS: "Suppress"
|
STR_IMAGES_SUPPRESS: "Suppress"
|
||||||
|
STR_EOB_HOME: "Home"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continue with"
|
||||||
STR_SHORT_PWR_BTN: "Short Power Button Click"
|
STR_SHORT_PWR_BTN: "Short Power Button Click"
|
||||||
STR_ORIENTATION: "Reading Orientation"
|
STR_ORIENTATION: "Reading Orientation"
|
||||||
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
||||||
@@ -78,6 +86,8 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
||||||
|
STR_LONG_PRESS_MENU: "Long-press Menu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "The quick brown fox jumps over the lazy dog"
|
||||||
STR_FONT_FAMILY: "Reader Font Family"
|
STR_FONT_FAMILY: "Reader Font Family"
|
||||||
STR_FONT_SIZE: "Reader Font Size"
|
STR_FONT_SIZE: "Reader Font Size"
|
||||||
STR_LINE_SPACING: "Reader Line Spacing"
|
STR_LINE_SPACING: "Reader Line Spacing"
|
||||||
@@ -97,6 +107,7 @@ STR_USERNAME: "Username"
|
|||||||
STR_PASSWORD: "Password"
|
STR_PASSWORD: "Password"
|
||||||
STR_SYNC_SERVER_URL: "Sync Server URL"
|
STR_SYNC_SERVER_URL: "Sync Server URL"
|
||||||
STR_DOCUMENT_MATCHING: "Document Matching"
|
STR_DOCUMENT_MATCHING: "Document Matching"
|
||||||
|
STR_SEND_METADATA: "Send Document Metadata"
|
||||||
STR_AUTHENTICATE: "Authenticate"
|
STR_AUTHENTICATE: "Authenticate"
|
||||||
STR_KOREADER_USERNAME: "KOReader Username"
|
STR_KOREADER_USERNAME: "KOReader Username"
|
||||||
STR_KOREADER_PASSWORD: "KOReader Password"
|
STR_KOREADER_PASSWORD: "KOReader Password"
|
||||||
@@ -137,9 +148,12 @@ STR_FORCE_REFRESH: "Refresh Screen"
|
|||||||
STR_PORTRAIT: "Portrait"
|
STR_PORTRAIT: "Portrait"
|
||||||
STR_LANDSCAPE_CW: "Landscape CW"
|
STR_LANDSCAPE_CW: "Landscape CW"
|
||||||
STR_INVERTED: "Inverted"
|
STR_INVERTED: "Inverted"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portrait 180°"
|
||||||
STR_LANDSCAPE_CCW: "Landscape CCW"
|
STR_LANDSCAPE_CCW: "Landscape CCW"
|
||||||
STR_PREV_NEXT: "Prev/Next"
|
STR_PREV_NEXT: "Prev/Next"
|
||||||
STR_NEXT_PREV: "Next/Prev"
|
STR_NEXT_PREV: "Next/Prev"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Bookmark"
|
||||||
STR_DISABLED: "Disabled"
|
STR_DISABLED: "Disabled"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
@@ -175,8 +189,7 @@ STR_DOWNLOADING: "Downloading..."
|
|||||||
STR_DOWNLOAD_FAILED: "Download failed"
|
STR_DOWNLOAD_FAILED: "Download failed"
|
||||||
STR_ERROR_MSG: "Error:"
|
STR_ERROR_MSG: "Error:"
|
||||||
STR_UNNAMED: "Unnamed"
|
STR_UNNAMED: "Unnamed"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Hold Confirm to Delete"
|
STR_HOLD_OPEN_TO_DELETE: "Hold Open to Delete"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Hold Confirm from the reader to create a bookmark."
|
|
||||||
STR_NO_SERVER_URL: "No server URL configured"
|
STR_NO_SERVER_URL: "No server URL configured"
|
||||||
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||||
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||||
@@ -194,6 +207,7 @@ STR_HOME: "« Home"
|
|||||||
STR_SELECT: "Select"
|
STR_SELECT: "Select"
|
||||||
STR_SELECTED: "Selected"
|
STR_SELECTED: "Selected"
|
||||||
STR_TOGGLE: "Toggle"
|
STR_TOGGLE: "Toggle"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Toggle Bookmark"
|
||||||
STR_CONFIRM: "Confirm"
|
STR_CONFIRM: "Confirm"
|
||||||
STR_CANCEL: "Cancel"
|
STR_CANCEL: "Cancel"
|
||||||
STR_CONNECT: "Connect"
|
STR_CONNECT: "Connect"
|
||||||
@@ -256,6 +270,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||||
STR_BOOKMARKS: "Bookmarks"
|
STR_BOOKMARKS: "Bookmarks"
|
||||||
STR_BOOKMARK_ADDED: "Bookmark added."
|
STR_BOOKMARK_ADDED: "Bookmark added."
|
||||||
|
STR_BOOKMARK_REMOVED: "Bookmark removed."
|
||||||
STR_OPDS_BROWSER: "OPDS Browser"
|
STR_OPDS_BROWSER: "OPDS Browser"
|
||||||
STR_SEARCH: "Search"
|
STR_SEARCH: "Search"
|
||||||
STR_COVER_CUSTOM: "Cover + Custom"
|
STR_COVER_CUSTOM: "Cover + Custom"
|
||||||
@@ -280,6 +295,25 @@ STR_HW_BACK_LABEL: "Back (1st button)"
|
|||||||
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
||||||
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
||||||
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
||||||
|
STR_BLUETOOTH: "Bluetooth"
|
||||||
|
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
|
||||||
|
STR_BT_SCAN_PAIR: "Scan & Pair"
|
||||||
|
STR_BT_NO_DEVICES: "No devices found"
|
||||||
|
STR_BT_FREE_HINT1: "Set Free2/3 to Reader Mode and"
|
||||||
|
STR_BT_FREE_HINT2: "Volume Function to pair"
|
||||||
|
STR_BT_DISCONNECT: "Disconnect"
|
||||||
|
STR_BT_CONNECTED_TO: "Connected: %s"
|
||||||
|
STR_BT_NOT_CONNECTED: "Not connected"
|
||||||
|
STR_BT_PAIRED_DEVICES: "Paired Devices"
|
||||||
|
STR_BT_NO_PAIRED: "No paired devices"
|
||||||
|
STR_BT_MAP_BUTTONS: "Map Remote Buttons"
|
||||||
|
STR_BT_PRESS_REMOTE: "Press a button on your remote"
|
||||||
|
STR_BT_CONNECTING_POPUP: "BT Connecting..."
|
||||||
|
STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)"
|
||||||
|
STR_STATE_PAUSED: "PAUSED"
|
||||||
|
STR_BT_PAGE_FORWARD: "Page Forward"
|
||||||
|
STR_BT_PAGE_BACK: "Page Back"
|
||||||
|
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
|
||||||
STR_GO_TO_PERCENT: "Go to %"
|
STR_GO_TO_PERCENT: "Go to %"
|
||||||
STR_GO_HOME_BUTTON: "Go Home"
|
STR_GO_HOME_BUTTON: "Go Home"
|
||||||
STR_SYNC_PROGRESS: "Sync Progress"
|
STR_SYNC_PROGRESS: "Sync Progress"
|
||||||
@@ -291,7 +325,6 @@ STR_CHAPTER_PREFIX: "Chapter: "
|
|||||||
STR_PAGES_SEPARATOR: " pages | "
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
STR_BOOK_PREFIX: "Book: "
|
STR_BOOK_PREFIX: "Book: "
|
||||||
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
|
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
|
||||||
STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%"
|
|
||||||
STR_SYNCING_TIME: "Syncing time..."
|
STR_SYNCING_TIME: "Syncing time..."
|
||||||
STR_CALC_HASH: "Calculating document hash..."
|
STR_CALC_HASH: "Calculating document hash..."
|
||||||
STR_HASH_FAILED: "Failed to calculate document hash"
|
STR_HASH_FAILED: "Failed to calculate document hash"
|
||||||
@@ -326,7 +359,8 @@ STR_LINK: "[link]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Take screenshot"
|
STR_SCREENSHOT_BUTTON: "Take screenshot"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Never"
|
STR_SLEEP_NEVER: "Never"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Left/Right: 1 min Up/Down: 5 min"
|
STR_STEP_HINT_FRONT: "Front buttons:"
|
||||||
|
STR_STEP_HINT_SIDE: "Side buttons:"
|
||||||
STR_ADD_SERVER: "Add Server"
|
STR_ADD_SERVER: "Add Server"
|
||||||
STR_SERVER_NAME: "Server Name"
|
STR_SERVER_NAME: "Server Name"
|
||||||
STR_NO_SERVERS: "No OPDS servers configured"
|
STR_NO_SERVERS: "No OPDS servers configured"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Törkylempijävongahdus"
|
||||||
STR_FONT_FAMILY: "Lukijan fonttiperhe"
|
STR_FONT_FAMILY: "Lukijan fonttiperhe"
|
||||||
STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
|
STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
|
||||||
STR_LINE_SPACING: "Lukijan riviväli"
|
STR_LINE_SPACING: "Lukijan riviväli"
|
||||||
@@ -91,6 +92,7 @@ STR_USERNAME: "Käyttäjänimi"
|
|||||||
STR_PASSWORD: "Salasana"
|
STR_PASSWORD: "Salasana"
|
||||||
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
|
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
|
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
|
||||||
|
STR_SEND_METADATA: "Lähetä asiakirjan metatiedot"
|
||||||
STR_AUTHENTICATE: "Tunnistaudu"
|
STR_AUTHENTICATE: "Tunnistaudu"
|
||||||
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
|
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
|
||||||
STR_KOREADER_PASSWORD: "KOReader-salasana"
|
STR_KOREADER_PASSWORD: "KOReader-salasana"
|
||||||
@@ -129,7 +131,8 @@ STR_SLEEP: "Lepotila"
|
|||||||
STR_PAGE_TURN: "Sivunkääntö"
|
STR_PAGE_TURN: "Sivunkääntö"
|
||||||
STR_PORTRAIT: "Pysty"
|
STR_PORTRAIT: "Pysty"
|
||||||
STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
|
STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
|
||||||
STR_INVERTED: "Käännetty"
|
STR_INVERTED: "Käänteinen"
|
||||||
|
STR_ORIENTATION_INVERTED: "Pysty 180°"
|
||||||
STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
|
STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
|
||||||
STR_PREV_NEXT: "Edell/Seur"
|
STR_PREV_NEXT: "Edell/Seur"
|
||||||
STR_NEXT_PREV: "Seur/Edell"
|
STR_NEXT_PREV: "Seur/Edell"
|
||||||
@@ -180,6 +183,9 @@ STR_EXIT: "« Poistu"
|
|||||||
STR_HOME: "« Koti"
|
STR_HOME: "« Koti"
|
||||||
STR_SELECT: "Valitse"
|
STR_SELECT: "Valitse"
|
||||||
STR_TOGGLE: "Vaihda"
|
STR_TOGGLE: "Vaihda"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Vaihda kirjanmerkki"
|
||||||
|
STR_BOOKMARK_REMOVED: "Kirjanmerkki poistettu."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Pidä Avaa painettuna poistaaksesi"
|
||||||
STR_CONFIRM: "Vahvista"
|
STR_CONFIRM: "Vahvista"
|
||||||
STR_CANCEL: "Peruuta"
|
STR_CANCEL: "Peruuta"
|
||||||
STR_CONNECT: "Yhdistä"
|
STR_CONNECT: "Yhdistä"
|
||||||
@@ -236,7 +242,6 @@ STR_CHAPTER_PREFIX: "Luku: "
|
|||||||
STR_PAGES_SEPARATOR: " sivua | "
|
STR_PAGES_SEPARATOR: " sivua | "
|
||||||
STR_BOOK_PREFIX: "Kirja: "
|
STR_BOOK_PREFIX: "Kirja: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen"
|
STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen"
|
||||||
STR_PERCENT_STEP_HINT: "Vasen/Oikea: 1% Ylös/Alas: 10%"
|
|
||||||
STR_SYNCING_TIME: "Synkronoidaan aikaa..."
|
STR_SYNCING_TIME: "Synkronoidaan aikaa..."
|
||||||
STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..."
|
STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..."
|
||||||
STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui"
|
STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui"
|
||||||
@@ -266,5 +271,8 @@ STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
|
|||||||
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
|
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Ei koskaan"
|
STR_SLEEP_NEVER: "Ei koskaan"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vasen/Oikea: 1 min Ylös/Alas: 5 min"
|
STR_STEP_HINT_FRONT: "Etupainikkeet:"
|
||||||
|
STR_STEP_HINT_SIDE: "Sivupainikkeet:"
|
||||||
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
|
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Lisää piilotettu verkko..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Anna verkon nimi (SSID)"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saut de chapitre"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saut de chapitre"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Changement d'orientation"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Changement d'orientation"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Portez ce vieux whisky au juge blond qui fume"
|
||||||
STR_FONT_FAMILY: "Police de caractères du lecteur"
|
STR_FONT_FAMILY: "Police de caractères du lecteur"
|
||||||
STR_FONT_SIZE: "Taille police lecteur"
|
STR_FONT_SIZE: "Taille police lecteur"
|
||||||
STR_LINE_SPACING: "Interligne"
|
STR_LINE_SPACING: "Interligne"
|
||||||
@@ -96,6 +97,7 @@ STR_USERNAME: "Nom d’utilisateur"
|
|||||||
STR_PASSWORD: "Mot de passe"
|
STR_PASSWORD: "Mot de passe"
|
||||||
STR_SYNC_SERVER_URL: "URL du serveur"
|
STR_SYNC_SERVER_URL: "URL du serveur"
|
||||||
STR_DOCUMENT_MATCHING: "Correspondance"
|
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||||
|
STR_SEND_METADATA: "Envoyer métadonnées"
|
||||||
STR_AUTHENTICATE: "Connexion"
|
STR_AUTHENTICATE: "Connexion"
|
||||||
STR_KOREADER_USERNAME: "Utilisateur"
|
STR_KOREADER_USERNAME: "Utilisateur"
|
||||||
STR_KOREADER_PASSWORD: "Mot de passe"
|
STR_KOREADER_PASSWORD: "Mot de passe"
|
||||||
@@ -135,6 +137,7 @@ STR_PAGE_TURN: "Page suivante"
|
|||||||
STR_PORTRAIT: "Portrait"
|
STR_PORTRAIT: "Portrait"
|
||||||
STR_LANDSCAPE_CW: "Paysage"
|
STR_LANDSCAPE_CW: "Paysage"
|
||||||
STR_INVERTED: "Inversé"
|
STR_INVERTED: "Inversé"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portrait 180°"
|
||||||
STR_LANDSCAPE_CCW: "Paysage inversé"
|
STR_LANDSCAPE_CCW: "Paysage inversé"
|
||||||
STR_PREV_NEXT: "Préc/Suiv"
|
STR_PREV_NEXT: "Préc/Suiv"
|
||||||
STR_NEXT_PREV: "Suiv/Préc"
|
STR_NEXT_PREV: "Suiv/Préc"
|
||||||
@@ -186,6 +189,9 @@ STR_HOME: "« Accueil"
|
|||||||
STR_SELECT: "OK"
|
STR_SELECT: "OK"
|
||||||
STR_SELECTED: "Sélectionné"
|
STR_SELECTED: "Sélectionné"
|
||||||
STR_TOGGLE: "Modifier"
|
STR_TOGGLE: "Modifier"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Basculer le marque-page"
|
||||||
|
STR_BOOKMARK_REMOVED: "Marque-page supprimé."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Maintenir Ouvrir pour supprimer"
|
||||||
STR_CONFIRM: "Confirmer"
|
STR_CONFIRM: "Confirmer"
|
||||||
STR_CANCEL: "Annuler"
|
STR_CANCEL: "Annuler"
|
||||||
STR_CONNECT: "Connecter"
|
STR_CONNECT: "Connecter"
|
||||||
@@ -262,7 +268,6 @@ STR_CHAPTER_PREFIX: "Chapitre : "
|
|||||||
STR_PAGES_SEPARATOR: " pages | "
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
STR_BOOK_PREFIX: "Livre : "
|
STR_BOOK_PREFIX: "Livre : "
|
||||||
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
||||||
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
|
|
||||||
STR_SYNCING_TIME: "Synchro de l’heure…"
|
STR_SYNCING_TIME: "Synchro de l’heure…"
|
||||||
STR_CALC_HASH: "Calcul hash doc…"
|
STR_CALC_HASH: "Calcul hash doc…"
|
||||||
STR_HASH_FAILED: "Échec calcul hash"
|
STR_HASH_FAILED: "Échec calcul hash"
|
||||||
@@ -294,8 +299,11 @@ STR_NO_FOOTNOTES: "Aucune note sur cette page"
|
|||||||
STR_LINK: "[lien]"
|
STR_LINK: "[lien]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Jamais"
|
STR_SLEEP_NEVER: "Jamais"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Gauche/Droite : 1 min Haut/Bas : 5 min"
|
STR_STEP_HINT_FRONT: "Boutons avant :"
|
||||||
|
STR_STEP_HINT_SIDE: "Boutons latéraux :"
|
||||||
STR_SCREENSHOT_BUTTON: "Capture d'écran"
|
STR_SCREENSHOT_BUTTON: "Capture d'écran"
|
||||||
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
|
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
|
||||||
STR_TILT_PAGE_TURN: "Tourner par inclinaison"
|
STR_TILT_PAGE_TURN: "Tourner par inclinaison"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Ajouter un réseau masqué..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Saisir le nom du réseau (SSID)"
|
||||||
|
|||||||
@@ -70,16 +70,18 @@ STR_ORIENTATION: "Leseausrichtung"
|
|||||||
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
|
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AUS"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Aus"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"
|
||||||
STR_FONT_FAMILY: "Lese-Schriftfamilie"
|
STR_FONT_FAMILY: "Lese-Schriftfamilie"
|
||||||
STR_FONT_SIZE: "Schriftgröße"
|
STR_FONT_SIZE: "Schriftgröße"
|
||||||
STR_LINE_SPACING: "Lese-Zeilenabstand"
|
STR_LINE_SPACING: "Lese-Zeilenabstand"
|
||||||
STR_SCREEN_MARGIN: "Lese-Seitenränder"
|
STR_SCREEN_MARGIN: "Lese-Seitenränder"
|
||||||
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
|
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
|
||||||
|
STR_LONG_PRESS_MENU: "Menütaste lang drücken"
|
||||||
STR_HYPHENATION: "Silbentrennung"
|
STR_HYPHENATION: "Silbentrennung"
|
||||||
STR_TIME_TO_SLEEP: "Standby nach"
|
STR_TIME_TO_SLEEP: "Standby-Modus nach"
|
||||||
STR_REFRESH_FREQ: "Anti-Ghosting nach"
|
STR_REFRESH_FREQ: "Anti-Ghosting nach"
|
||||||
STR_KOREADER_SYNC: "KOReader-Synchr."
|
STR_KOREADER_SYNC: "KOReader-Synchr."
|
||||||
STR_CHECK_UPDATES: "Nach Updates suchen"
|
STR_CHECK_UPDATES: "Nach Updates suchen"
|
||||||
@@ -89,6 +91,7 @@ STR_USERNAME: "Benutzername"
|
|||||||
STR_PASSWORD: "Passwort"
|
STR_PASSWORD: "Passwort"
|
||||||
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
||||||
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
||||||
|
STR_SEND_METADATA: "Metadaten senden"
|
||||||
STR_AUTHENTICATE: "Authentifizieren"
|
STR_AUTHENTICATE: "Authentifizieren"
|
||||||
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
||||||
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
||||||
@@ -128,7 +131,8 @@ STR_PAGE_TURN: "Umblättern"
|
|||||||
STR_FORCE_REFRESH: "Bildschirm regenerieren"
|
STR_FORCE_REFRESH: "Bildschirm regenerieren"
|
||||||
STR_PORTRAIT: "Hochformat"
|
STR_PORTRAIT: "Hochformat"
|
||||||
STR_LANDSCAPE_CW: "Querformat rechts"
|
STR_LANDSCAPE_CW: "Querformat rechts"
|
||||||
STR_INVERTED: "Hochformat 180°"
|
STR_INVERTED: "Invertiert"
|
||||||
|
STR_ORIENTATION_INVERTED: "Hochformat 180°"
|
||||||
STR_LANDSCAPE_CCW: "Querformat links"
|
STR_LANDSCAPE_CCW: "Querformat links"
|
||||||
STR_PREV_NEXT: "Zurück/Weiter"
|
STR_PREV_NEXT: "Zurück/Weiter"
|
||||||
STR_NEXT_PREV: "Weiter/Zurück"
|
STR_NEXT_PREV: "Weiter/Zurück"
|
||||||
@@ -167,8 +171,7 @@ STR_DOWNLOADING: "Herunterladen…"
|
|||||||
STR_DOWNLOAD_FAILED: "Ladefehler"
|
STR_DOWNLOAD_FAILED: "Ladefehler"
|
||||||
STR_ERROR_MSG: "Fehler:"
|
STR_ERROR_MSG: "Fehler:"
|
||||||
STR_UNNAMED: "Unbenannt"
|
STR_UNNAMED: "Unbenannt"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Halte Bestätigen zum Löschen"
|
STR_HOLD_OPEN_TO_DELETE: "Halte Öffnen zum Löschen"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Halte Bestätigen im Lesemodus um ein Lesezeichen anzulegen."
|
|
||||||
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
|
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
|
||||||
STR_FETCH_FEED_FAILED: "Feedfehler"
|
STR_FETCH_FEED_FAILED: "Feedfehler"
|
||||||
STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
|
STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
|
||||||
@@ -186,6 +189,7 @@ STR_HOME: "« Start"
|
|||||||
STR_SELECT: "Auswahl"
|
STR_SELECT: "Auswahl"
|
||||||
STR_SELECTED: "Ausgewählt"
|
STR_SELECTED: "Ausgewählt"
|
||||||
STR_TOGGLE: "Ändern"
|
STR_TOGGLE: "Ändern"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Lesezeichen umschalten"
|
||||||
STR_CONFIRM: "Bestätigen"
|
STR_CONFIRM: "Bestätigen"
|
||||||
STR_CANCEL: "Abbrechen"
|
STR_CANCEL: "Abbrechen"
|
||||||
STR_CONNECT: "Verbinden"
|
STR_CONNECT: "Verbinden"
|
||||||
@@ -249,6 +253,7 @@ STR_QUICK_RESUME_TIMEOUT: "Schnelles Fortsetzen nach Timeout"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
|
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
|
||||||
STR_BOOKMARKS: "Lesezeichen"
|
STR_BOOKMARKS: "Lesezeichen"
|
||||||
STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt."
|
STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt."
|
||||||
|
STR_BOOKMARK_REMOVED: "Lesezeichen entfernt."
|
||||||
STR_SEARCH: "Suche"
|
STR_SEARCH: "Suche"
|
||||||
STR_OPDS_BROWSER: "OPDS-Browser"
|
STR_OPDS_BROWSER: "OPDS-Browser"
|
||||||
STR_COVER_CUSTOM: "Cover + Eigenes"
|
STR_COVER_CUSTOM: "Cover + Eigenes"
|
||||||
@@ -284,7 +289,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
|
|||||||
STR_PAGES_SEPARATOR: " Seiten | "
|
STR_PAGES_SEPARATOR: " Seiten | "
|
||||||
STR_BOOK_PREFIX: "Buch: "
|
STR_BOOK_PREFIX: "Buch: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
|
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
|
||||||
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
|
|
||||||
STR_SYNCING_TIME: "Zeit synchronisieren…"
|
STR_SYNCING_TIME: "Zeit synchronisieren…"
|
||||||
STR_CALC_HASH: "Dokument-Hash berechnen…"
|
STR_CALC_HASH: "Dokument-Hash berechnen…"
|
||||||
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
|
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
|
||||||
@@ -318,7 +322,8 @@ STR_NO_FOOTNOTES: "Keine Fußnoten auf dieser Seite"
|
|||||||
STR_LINK: "[Link]"
|
STR_LINK: "[Link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min."
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min."
|
||||||
STR_SLEEP_NEVER: "Nie"
|
STR_SLEEP_NEVER: "Nie"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 Min. Hoch/Runter: 5 Min."
|
STR_STEP_HINT_FRONT: "Vordere Tasten:"
|
||||||
|
STR_STEP_HINT_SIDE: "Seitentasten:"
|
||||||
STR_ADD_SERVER: "Server hinzufügen"
|
STR_ADD_SERVER: "Server hinzufügen"
|
||||||
STR_SERVER_NAME: "Servername"
|
STR_SERVER_NAME: "Servername"
|
||||||
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
|
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
|
||||||
@@ -376,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Schreiben der Firmware-Datei ist fehlgeschlagen"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
|
||||||
STR_RECOVERY_MODE: "Wiederherstellungsmodus"
|
STR_RECOVERY_MODE: "Wiederherstellungsmodus"
|
||||||
STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus"
|
STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Verstecktes Netzwerk hinzufügen..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Netzwerknamen eingeben (SSID)"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "שנה כיוון מסך"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "שנה כיוון מסך"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "דג סקרן שט בים מאוכזב ולפתע מצא חברה"
|
||||||
STR_FONT_FAMILY: "גופן הקריאה"
|
STR_FONT_FAMILY: "גופן הקריאה"
|
||||||
STR_FONT_SIZE: "גודל גופן"
|
STR_FONT_SIZE: "גודל גופן"
|
||||||
STR_LINE_SPACING: "מרווח בין שורות"
|
STR_LINE_SPACING: "מרווח בין שורות"
|
||||||
@@ -94,6 +95,7 @@ STR_USERNAME: "שם משתמש"
|
|||||||
STR_PASSWORD: "סיסמה"
|
STR_PASSWORD: "סיסמה"
|
||||||
STR_SYNC_SERVER_URL: "כתובת שרת סנכרון"
|
STR_SYNC_SERVER_URL: "כתובת שרת סנכרון"
|
||||||
STR_DOCUMENT_MATCHING: "התאמת מסמכים"
|
STR_DOCUMENT_MATCHING: "התאמת מסמכים"
|
||||||
|
STR_SEND_METADATA: "שלח מטא-נתוני מסמך"
|
||||||
STR_AUTHENTICATE: "התחבר"
|
STR_AUTHENTICATE: "התחבר"
|
||||||
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
|
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
|
||||||
STR_KOREADER_PASSWORD: "סיסמת KOReader"
|
STR_KOREADER_PASSWORD: "סיסמת KOReader"
|
||||||
@@ -133,7 +135,8 @@ STR_PAGE_TURN: "העברת דף"
|
|||||||
STR_FORCE_REFRESH: "רענון מסך מלא"
|
STR_FORCE_REFRESH: "רענון מסך מלא"
|
||||||
STR_PORTRAIT: "לאורך"
|
STR_PORTRAIT: "לאורך"
|
||||||
STR_LANDSCAPE_CW: "לרוחב (ימינה)"
|
STR_LANDSCAPE_CW: "לרוחב (ימינה)"
|
||||||
STR_INVERTED: "הפוך"
|
STR_INVERTED: "היפוך צבעים"
|
||||||
|
STR_ORIENTATION_INVERTED: "לאורך 180°"
|
||||||
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
|
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
|
||||||
STR_PREV_NEXT: "הקודם/הבא"
|
STR_PREV_NEXT: "הקודם/הבא"
|
||||||
STR_NEXT_PREV: "הבא/הקודם"
|
STR_NEXT_PREV: "הבא/הקודם"
|
||||||
@@ -188,6 +191,7 @@ STR_HOME: "מסך הבית »"
|
|||||||
STR_SELECT: "בחר"
|
STR_SELECT: "בחר"
|
||||||
STR_SELECTED: "נבחר"
|
STR_SELECTED: "נבחר"
|
||||||
STR_TOGGLE: "בחר"
|
STR_TOGGLE: "בחר"
|
||||||
|
STR_TOGGLE_BOOKMARK: "הוסף/הסר סימנייה"
|
||||||
STR_CONFIRM: "אישור"
|
STR_CONFIRM: "אישור"
|
||||||
STR_CANCEL: "ביטול"
|
STR_CANCEL: "ביטול"
|
||||||
STR_CONNECT: "התחבר"
|
STR_CONNECT: "התחבר"
|
||||||
@@ -265,7 +269,6 @@ STR_CHAPTER_PREFIX: "פרק: "
|
|||||||
STR_PAGES_SEPARATOR: " דפים | "
|
STR_PAGES_SEPARATOR: " דפים | "
|
||||||
STR_BOOK_PREFIX: "ספר: "
|
STR_BOOK_PREFIX: "ספר: "
|
||||||
STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL"
|
STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL"
|
||||||
STR_PERCENT_STEP_HINT: "שמאל/ימין: 1% למעלה/למטה: 10%"
|
|
||||||
STR_SYNCING_TIME: "מסנכרן שעון..."
|
STR_SYNCING_TIME: "מסנכרן שעון..."
|
||||||
STR_CALC_HASH: "מחשב האש..."
|
STR_CALC_HASH: "מחשב האש..."
|
||||||
STR_HASH_FAILED: "חישוב האש נכשל"
|
STR_HASH_FAILED: "חישוב האש נכשל"
|
||||||
@@ -359,8 +362,7 @@ STR_FRONT_BTN_FOLLOW_ORIENTATION: "התאמת לחצנים קדמיים לכיו
|
|||||||
STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים"
|
STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים"
|
||||||
STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'"
|
STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'"
|
||||||
STR_DISABLED: "מבוטל"
|
STR_DISABLED: "מבוטל"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "החזק לחוץ על אישור כדי למחוק"
|
STR_HOLD_OPEN_TO_DELETE: "לחיצה ארוכה על 'פתח' כדי למחוק"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "החזק לחוץ על כפתור אישור בזמן הקריאה כדי להוסיף סימנייה"
|
|
||||||
STR_CLOCK: "שעון"
|
STR_CLOCK: "שעון"
|
||||||
STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC"
|
STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC"
|
||||||
STR_CLOCK_FORMAT: "תבנית השעון"
|
STR_CLOCK_FORMAT: "תבנית השעון"
|
||||||
@@ -378,6 +380,17 @@ STR_CLOCK_SYNC_NO_WIFI_HINT: "התחבר תחילה לרשת אלחוטית, ו
|
|||||||
STR_CLOCK_SYNCED: "השעון סונכרן"
|
STR_CLOCK_SYNCED: "השעון סונכרן"
|
||||||
STR_BOOKMARKS: "סימניות"
|
STR_BOOKMARKS: "סימניות"
|
||||||
STR_BOOKMARK_ADDED: "הסימנייה התווספה"
|
STR_BOOKMARK_ADDED: "הסימנייה התווספה"
|
||||||
|
STR_BOOKMARK_REMOVED: "הסימנייה הוסרה"
|
||||||
STR_QUICK_RESUME: "חזרה מהירה"
|
STR_QUICK_RESUME: "חזרה מהירה"
|
||||||
STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?"
|
STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?"
|
STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?"
|
||||||
|
STR_LONG_PRESS_MENU: "לחיצה ארוכה על אישור"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "סימנייה"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "חזרה מהירה מהערות שוליים"
|
||||||
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
|
||||||
|
STR_SLEEP_NEVER: "אף פעם"
|
||||||
|
STR_STEP_HINT_FRONT: "לחצנים קדמיים:"
|
||||||
|
STR_STEP_HINT_SIDE: "לחצני צד:"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "הוסף רשת מוסתרת..."
|
||||||
|
STR_ENTER_WIFI_SSID: "הזן שם רשת (SSID)"
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
|
|||||||
STR_MAC_ADDRESS: "MAC-cím:"
|
STR_MAC_ADDRESS: "MAC-cím:"
|
||||||
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
|
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
|
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Rejtett hálózat hozzáadása..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Add meg a hálózat nevét (SSID)"
|
||||||
STR_TO_PREFIX: "- "
|
STR_TO_PREFIX: "- "
|
||||||
STR_CALIBRE_RECEIVING: "Fogadás: "
|
STR_CALIBRE_RECEIVING: "Fogadás: "
|
||||||
STR_CALIBRE_RECEIVED: "Fogadva: "
|
STR_CALIBRE_RECEIVED: "Fogadva: "
|
||||||
@@ -74,6 +76,7 @@ STR_ORIENTATION: "Olvasási irány"
|
|||||||
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
|
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
|
||||||
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
|
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban"
|
||||||
STR_FONT_FAMILY: "Olvasó betűkészlet"
|
STR_FONT_FAMILY: "Olvasó betűkészlet"
|
||||||
STR_FONT_SIZE: "Olvasó betűméret"
|
STR_FONT_SIZE: "Olvasó betűméret"
|
||||||
STR_LINE_SPACING: "Olvasó sorköz"
|
STR_LINE_SPACING: "Olvasó sorköz"
|
||||||
@@ -93,6 +96,7 @@ STR_USERNAME: "Felhasználónév"
|
|||||||
STR_PASSWORD: "Jelszó"
|
STR_PASSWORD: "Jelszó"
|
||||||
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
|
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
|
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
|
||||||
|
STR_SEND_METADATA: "Dokumentum metaadatok küldése"
|
||||||
STR_AUTHENTICATE: "Hitelesítés"
|
STR_AUTHENTICATE: "Hitelesítés"
|
||||||
STR_KOREADER_USERNAME: "KOReader felhasználónév"
|
STR_KOREADER_USERNAME: "KOReader felhasználónév"
|
||||||
STR_KOREADER_PASSWORD: "KOReader jelszó"
|
STR_KOREADER_PASSWORD: "KOReader jelszó"
|
||||||
@@ -131,7 +135,8 @@ STR_SLEEP: "Alvás"
|
|||||||
STR_PAGE_TURN: "Lapozás"
|
STR_PAGE_TURN: "Lapozás"
|
||||||
STR_PORTRAIT: "Álló"
|
STR_PORTRAIT: "Álló"
|
||||||
STR_LANDSCAPE_CW: "Fekvő jobbra"
|
STR_LANDSCAPE_CW: "Fekvő jobbra"
|
||||||
STR_INVERTED: "Fordított"
|
STR_INVERTED: "Invertált"
|
||||||
|
STR_ORIENTATION_INVERTED: "Álló 180°"
|
||||||
STR_LANDSCAPE_CCW: "Fekvő balra"
|
STR_LANDSCAPE_CCW: "Fekvő balra"
|
||||||
STR_PREV_NEXT: "Előző/Következő"
|
STR_PREV_NEXT: "Előző/Következő"
|
||||||
STR_NEXT_PREV: "Következő/Előző"
|
STR_NEXT_PREV: "Következő/Előző"
|
||||||
@@ -183,6 +188,9 @@ STR_HOME: "« Főoldal"
|
|||||||
STR_SELECT: "Kiválasztás"
|
STR_SELECT: "Kiválasztás"
|
||||||
STR_SELECTED: "Kiválasztva"
|
STR_SELECTED: "Kiválasztva"
|
||||||
STR_TOGGLE: "Váltás"
|
STR_TOGGLE: "Váltás"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Könyvjelző váltása"
|
||||||
|
STR_BOOKMARK_REMOVED: "Könyvjelző eltávolítva."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Tartsa lenyomva a Megnyitás gombot a törléshez"
|
||||||
STR_CONFIRM: "Megerősítés"
|
STR_CONFIRM: "Megerősítés"
|
||||||
STR_CANCEL: "Mégse"
|
STR_CANCEL: "Mégse"
|
||||||
STR_CONNECT: "Csatlakozás"
|
STR_CONNECT: "Csatlakozás"
|
||||||
@@ -258,7 +266,6 @@ STR_CHAPTER_PREFIX: "Fejezet: "
|
|||||||
STR_PAGES_SEPARATOR: " oldal | "
|
STR_PAGES_SEPARATOR: " oldal | "
|
||||||
STR_BOOK_PREFIX: "Könyv: "
|
STR_BOOK_PREFIX: "Könyv: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez"
|
STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez"
|
||||||
STR_PERCENT_STEP_HINT: "Bal/Jobb: 1% Fel/Le: 10%"
|
|
||||||
STR_SYNCING_TIME: "Idő szinkronizálása..."
|
STR_SYNCING_TIME: "Idő szinkronizálása..."
|
||||||
STR_CALC_HASH: "Dokumentum hash kiszámítása..."
|
STR_CALC_HASH: "Dokumentum hash kiszámítása..."
|
||||||
STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen"
|
STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen"
|
||||||
@@ -290,8 +297,99 @@ STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
|
||||||
STR_SLEEP_NEVER: "Soha"
|
STR_SLEEP_NEVER: "Soha"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Bal/Jobb: 1 perc Fel/Le: 5 perc"
|
STR_STEP_HINT_FRONT: "Elülső gombok:"
|
||||||
|
STR_STEP_HINT_SIDE: "Oldalsó gombok:"
|
||||||
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
|
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
|
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
|
||||||
STR_TILT_PAGE_TURN: "Döntéses lapozás"
|
STR_TILT_PAGE_TURN: "Döntéses lapozás"
|
||||||
|
STR_ADD_SERVER: "Szerver hozzáadása"
|
||||||
|
STR_BOOKMARKS: "Könyvjelzők"
|
||||||
|
STR_BOOKMARK_ADDED: "Könyvjelző hozzáadva."
|
||||||
|
STR_BOOKMARK_OPTION: "Könyvjelző"
|
||||||
|
STR_BOTTOM: "Alul"
|
||||||
|
STR_CLOCK: "Óra"
|
||||||
|
STR_CLOCK_FORMAT: "Óraformátum"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 órás"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 órás"
|
||||||
|
STR_CLOCK_SYNC: "Óra szinkronizálása"
|
||||||
|
STR_CLOCK_SYNCED: "Óra szinkronizálva"
|
||||||
|
STR_CLOCK_SYNCING: "Szinkronizálás NTP-ről..."
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Szinkronizálás sikertelen"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Óra szinkronizálása most"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nincs csatlakoztatva"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Először csatlakozz Wi-Fi-hez, majd próbáld újra."
|
||||||
|
STR_CLOCK_SYNC_OK: "Óra szinkronizálva"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Óra UTC eltolás"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Törlöd ezt a könyvjelzőt?"
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Csatlakozás mentett Wi-Fi-hez..."
|
||||||
|
STR_CRASH_DESCRIPTION: "A részletes jelentés a crash_report.txt fájlba lett mentve. Kérjük, csatold ezt a fájlt a hibajelentéshez."
|
||||||
|
STR_CRASH_NO_REASON: "(Nem került rögzítésre ok)"
|
||||||
|
STR_CRASH_REASON: "Összeomlás oka:"
|
||||||
|
STR_CRASH_TITLE: "Rendszerösszeomlás"
|
||||||
|
STR_CURRENT_TIME: "Aktuális idő:"
|
||||||
|
STR_DELETE_SERVER: "Szerver törlése"
|
||||||
|
STR_DISABLED: "Letiltva"
|
||||||
|
STR_DOWNLOAD_ALL: "Összes letöltése"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Folytatás ezzel"
|
||||||
|
STR_EOB_HOME: "Kezdőképernyő"
|
||||||
|
STR_FINDING_SAVED_WIFI: "Mentett Wi-Fi keresése..."
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "A fájl nem nyitható meg"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "A firmware túl nagy a partícióhoz"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "A firmware fájl túl kicsi"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne kapcsold ki!"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Frissíted a firmware-t?"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "A firmware írása sikertelen"
|
||||||
|
STR_FONT_BROWSER: "Betűtípus-böngésző"
|
||||||
|
STR_FONT_INSTALLED: "Betűtípus telepítve!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "A betűtípus telepítése sikertelen"
|
||||||
|
STR_FORCE_REFRESH: "Képernyő frissítése"
|
||||||
|
STR_INDEX_FAILED: "Indexelés sikertelen – érvénytelen könyv"
|
||||||
|
STR_INSTALLED: "Telepítve"
|
||||||
|
STR_INVALID_FIRMWARE: "Érvénytelen firmware fájl"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Tartsd nyomva a DEL-t az összes szöveg törléséhez"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Tartsd nyomva a FEL-t a bejegyzés szerkesztéséhez"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Nyomd meg az ABC-t az URL mód elhagyásához"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg a [***]-ot a jelszó elrejtéséhez"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Tartsd nyomva a SELECT-et kisbetűért vagy másodlagos karakterért"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Nyomd meg a BALRA vagy JOBBRA gombot a kurzor mozgatásához"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Nyomd meg a BALRA-t a kurzorpozícióhoz való visszatéréshez"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Nyomd meg a LE-t a billentyűzethez való visszatéréshez"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Tartsd nyomva a SELECT-et másodlagos karakterért"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg az [abc]-t a jelszó megjelenítéséhez"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Nyomd meg a [***]-ot a jelszó elrejtéséhez"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Nyomd meg az [abc]-t a jelszó megjelenítéséhez"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Tartsd nyomva a SELECT-et NAGYBETŰÉRT vagy másodlagos karakterért"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Nyomd meg az URL-t az URL-részletekért"
|
||||||
|
STR_KB_TIPS: "Tippek:"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_LOADING_FONT_LIST: "Betűtípuslista betöltése..."
|
||||||
|
STR_LONG_PRESS_BEHAVIOR: "Hosszú gombnyomás viselkedése"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_OFF: "KI"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Tájolásváltás"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Fejezetugrás"
|
||||||
|
STR_LONG_PRESS_MENU: "Hosszú nyomás – Menü"
|
||||||
|
STR_MANAGE_FONTS: "Betűtípusok kezelése"
|
||||||
|
STR_NEXT_FIELD: "Következő"
|
||||||
|
STR_NEXT_PAGE: "Következő oldal »"
|
||||||
|
STR_NO_BIN_FILES: "Nincs .bin fájl"
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Nincs elérhető betűtípus"
|
||||||
|
STR_NO_SERVERS: "Nincs beállított OPDS szerver"
|
||||||
|
STR_OPDS_SERVERS: "OPDS szerverek"
|
||||||
|
STR_PREV_PAGE: "« Előző oldal"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Gyors visszatérés lábjegyzetből"
|
||||||
|
STR_RECOVERY_MODE: "Helyreállítási mód"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Helyezd a firmware.bin fájlt az SD-kártya gyökerébe, majd válaszd ki"
|
||||||
|
STR_RESTARTING_HINT: "Újraindítás... Ha az eszköz nem indul újra, tartsd nyomva a bekapcsológombot néhány másodpercig."
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Firmware frissítés SD-kártyáról"
|
||||||
|
STR_SEARCH: "Keresés"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Válassz firmware fájlt (.bin)"
|
||||||
|
STR_SERVER_NAME: "Szerver neve"
|
||||||
|
STR_SET_SLEEP_COVER: "Borító beállítása"
|
||||||
|
STR_SHOW_NETWORKS: "Megjelenítés"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_TOP: "Felül"
|
||||||
|
STR_UPDATE_ALL: "Összes frissítése"
|
||||||
|
STR_UPDATE_AVAILABLE: "Frissítés"
|
||||||
|
STR_VALIDATING_FIRMWARE: "Firmware ellenőrzése..."
|
||||||
|
STR_XTC_STATUS_BAR: "XTC állapotsáv"
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Salta capitolo"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Salta capitolo"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientamento"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientamento"
|
||||||
|
STR_LONG_PRESS_MENU: "Menu press. lunga"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Pranzo d'acqua fa volti sghembi"
|
||||||
STR_FONT_FAMILY: "Font lettore"
|
STR_FONT_FAMILY: "Font lettore"
|
||||||
STR_FONT_SIZE: "Dimensione font"
|
STR_FONT_SIZE: "Dimensione font"
|
||||||
STR_LINE_SPACING: "Interlinea lettore"
|
STR_LINE_SPACING: "Interlinea lettore"
|
||||||
@@ -96,6 +98,7 @@ STR_USERNAME: "Nome utente"
|
|||||||
STR_PASSWORD: "Password"
|
STR_PASSWORD: "Password"
|
||||||
STR_SYNC_SERVER_URL: "URL server di sincronizzazione"
|
STR_SYNC_SERVER_URL: "URL server di sincronizzazione"
|
||||||
STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
|
STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
|
||||||
|
STR_SEND_METADATA: "Invia metadati documento"
|
||||||
STR_AUTHENTICATE: "Autentica"
|
STR_AUTHENTICATE: "Autentica"
|
||||||
STR_KOREADER_USERNAME: "Nome utente KOReader"
|
STR_KOREADER_USERNAME: "Nome utente KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Password KOReader"
|
STR_KOREADER_PASSWORD: "Password KOReader"
|
||||||
@@ -135,7 +138,8 @@ STR_PAGE_TURN: "Cambio pagina"
|
|||||||
STR_FORCE_REFRESH: "Refresh"
|
STR_FORCE_REFRESH: "Refresh"
|
||||||
STR_PORTRAIT: "Verticale"
|
STR_PORTRAIT: "Verticale"
|
||||||
STR_LANDSCAPE_CW: "Orizzontale Dx"
|
STR_LANDSCAPE_CW: "Orizzontale Dx"
|
||||||
STR_INVERTED: "Capovolto"
|
STR_INVERTED: "Invertito"
|
||||||
|
STR_ORIENTATION_INVERTED: "Verticale 180°"
|
||||||
STR_LANDSCAPE_CCW: "Orizzontale Sx"
|
STR_LANDSCAPE_CCW: "Orizzontale Sx"
|
||||||
STR_PREV_NEXT: "Prec/Succ"
|
STR_PREV_NEXT: "Prec/Succ"
|
||||||
STR_NEXT_PREV: "Succ/Prec"
|
STR_NEXT_PREV: "Succ/Prec"
|
||||||
@@ -190,6 +194,7 @@ STR_HOME: "« Home"
|
|||||||
STR_SELECT: "Seleziona"
|
STR_SELECT: "Seleziona"
|
||||||
STR_SELECTED: "Selezionato"
|
STR_SELECTED: "Selezionato"
|
||||||
STR_TOGGLE: "Cambia"
|
STR_TOGGLE: "Cambia"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Attiva/disattiva segnalibro"
|
||||||
STR_CONFIRM: "Conferma"
|
STR_CONFIRM: "Conferma"
|
||||||
STR_CANCEL: "Annulla"
|
STR_CANCEL: "Annulla"
|
||||||
STR_CONNECT: "Connetti"
|
STR_CONNECT: "Connetti"
|
||||||
@@ -270,7 +275,6 @@ STR_CHAPTER_PREFIX: "Capitolo: "
|
|||||||
STR_PAGES_SEPARATOR: " pagine | "
|
STR_PAGES_SEPARATOR: " pagine | "
|
||||||
STR_BOOK_PREFIX: "Libro: "
|
STR_BOOK_PREFIX: "Libro: "
|
||||||
STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL"
|
STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL"
|
||||||
STR_PERCENT_STEP_HINT: "Sinistra/Destra: 1% Su/Giù: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sincronizzazione orario..."
|
STR_SYNCING_TIME: "Sincronizzazione orario..."
|
||||||
STR_CALC_HASH: "Calcolo hash documento..."
|
STR_CALC_HASH: "Calcolo hash documento..."
|
||||||
STR_HASH_FAILED: "Impossibile calcolare l'hash del documento"
|
STR_HASH_FAILED: "Impossibile calcolare l'hash del documento"
|
||||||
@@ -303,7 +307,8 @@ STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Mai"
|
STR_SLEEP_NEVER: "Mai"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Sinistra/Destra: 1 min Su/Giù: 5 min"
|
STR_STEP_HINT_FRONT: "Pulsanti frontali:"
|
||||||
|
STR_STEP_HINT_SIDE: "Pulsanti laterali:"
|
||||||
STR_SCREENSHOT_BUTTON: "Screenshot"
|
STR_SCREENSHOT_BUTTON: "Screenshot"
|
||||||
STR_ADD_SERVER: "Aggiungi server"
|
STR_ADD_SERVER: "Aggiungi server"
|
||||||
STR_SERVER_NAME: "Nome server"
|
STR_SERVER_NAME: "Nome server"
|
||||||
@@ -355,9 +360,9 @@ STR_FIRMWARE_WRITE_FAILED: "Aggiornamento firmware non riuscito"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!"
|
||||||
STR_RECOVERY_MODE: "Modalità ripristino"
|
STR_RECOVERY_MODE: "Modalità ripristino"
|
||||||
STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo"
|
STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Tieni premuto Conferma nel lettore per creare un segnalibro"
|
|
||||||
STR_BOOKMARKS: "Segnalibri"
|
STR_BOOKMARKS: "Segnalibri"
|
||||||
STR_BOOKMARK_ADDED: "Segnalibro aggiunto"
|
STR_BOOKMARK_ADDED: "Segnalibro aggiunto"
|
||||||
|
STR_BOOKMARK_REMOVED: "Segnalibro rimosso"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?"
|
STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?"
|
||||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova"
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova"
|
||||||
STR_CLOCK_SYNC_OK: "Orologio sincronizzato"
|
STR_CLOCK_SYNC_OK: "Orologio sincronizzato"
|
||||||
@@ -372,7 +377,15 @@ STR_CLOCK_SYNCING: "Sincronizzazione con il server NTP..."
|
|||||||
STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita"
|
STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita"
|
||||||
STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso"
|
STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso"
|
||||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso"
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Tieni premuto Conferma per cancellare"
|
STR_HOLD_OPEN_TO_DELETE: "Tieni premuto Apri per eliminare"
|
||||||
STR_NEXT_FIELD: "Succ."
|
STR_NEXT_FIELD: "Succ."
|
||||||
STR_CURRENT_TIME: "Ora attuale: "
|
STR_CURRENT_TIME: "Ora attuale: "
|
||||||
STR_DISABLED: "Disattivato"
|
STR_DISABLED: "Disattivato"
|
||||||
|
STR_BOOKMARK_OPTION: "Segnalibro"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continua con"
|
||||||
|
STR_EOB_HOME: "Home"
|
||||||
|
STR_INDEX_FAILED: "Indicizzazione fallita - libro non valido"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Aggiungi rete nascosta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Inserisci il nome della rete (SSID)"
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ STR_ORIENTATION: "Оқу бағдары"
|
|||||||
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
|
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
|
||||||
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
|
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
|
||||||
STR_FONT_FAMILY: "Оқырман қаріп тобы"
|
STR_FONT_FAMILY: "Оқырман қаріп тобы"
|
||||||
STR_FONT_SIZE: "Интерфейс қаріп өлшемі"
|
STR_FONT_SIZE: "Интерфейс қаріп өлшемі"
|
||||||
STR_LINE_SPACING: "Оқырман жол аралығы"
|
STR_LINE_SPACING: "Оқырман жол аралығы"
|
||||||
@@ -87,6 +88,7 @@ STR_USERNAME: "Пайдаланушы аты"
|
|||||||
STR_PASSWORD: "Құпия сөз"
|
STR_PASSWORD: "Құпия сөз"
|
||||||
STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
||||||
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
||||||
|
STR_SEND_METADATA: "Құжат метадеректерін жіберу"
|
||||||
STR_AUTHENTICATE: "Аутентификация"
|
STR_AUTHENTICATE: "Аутентификация"
|
||||||
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
||||||
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
||||||
@@ -125,7 +127,8 @@ STR_SLEEP: "Ұйқы"
|
|||||||
STR_PAGE_TURN: "Бет аудару"
|
STR_PAGE_TURN: "Бет аудару"
|
||||||
STR_PORTRAIT: "Тік бағдар"
|
STR_PORTRAIT: "Тік бағдар"
|
||||||
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
|
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
|
||||||
STR_INVERTED: "Төңкерілген"
|
STR_INVERTED: "Инверсия"
|
||||||
|
STR_ORIENTATION_INVERTED: "Тік бағдар 180°"
|
||||||
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
|
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
|
||||||
STR_PREV_NEXT: "Алдыңғы/Келесі"
|
STR_PREV_NEXT: "Алдыңғы/Келесі"
|
||||||
STR_NEXT_PREV: "Келесі/Алдыңғы"
|
STR_NEXT_PREV: "Келесі/Алдыңғы"
|
||||||
@@ -176,6 +179,9 @@ STR_EXIT: "« Шығу"
|
|||||||
STR_HOME: "« Басты"
|
STR_HOME: "« Басты"
|
||||||
STR_SELECT: "Таңдау"
|
STR_SELECT: "Таңдау"
|
||||||
STR_TOGGLE: "Ауыстыру"
|
STR_TOGGLE: "Ауыстыру"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Бетбелгіні ауыстыру"
|
||||||
|
STR_BOOKMARK_REMOVED: "Бетбелгі жойылды."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Жою үшін Ашу түймесін ұстап тұрыңыз"
|
||||||
STR_CONFIRM: "Растау"
|
STR_CONFIRM: "Растау"
|
||||||
STR_CANCEL: "Болдырмау"
|
STR_CANCEL: "Болдырмау"
|
||||||
STR_CONNECT: "Қосылу"
|
STR_CONNECT: "Қосылу"
|
||||||
@@ -232,7 +238,6 @@ STR_CHAPTER_PREFIX: "Тарау: "
|
|||||||
STR_PAGES_SEPARATOR: " бет | "
|
STR_PAGES_SEPARATOR: " бет | "
|
||||||
STR_BOOK_PREFIX: "Кітап: "
|
STR_BOOK_PREFIX: "Кітап: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
|
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
|
||||||
STR_PERCENT_STEP_HINT: "Сол/Оң: 1% Жоғары/Төмен: 10%"
|
|
||||||
STR_SYNCING_TIME: "Уақыт синхрондалуда..."
|
STR_SYNCING_TIME: "Уақыт синхрондалуда..."
|
||||||
STR_CALC_HASH: "Құжат хэші есептелуде..."
|
STR_CALC_HASH: "Құжат хэші есептелуде..."
|
||||||
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
|
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
|
||||||
@@ -289,8 +294,11 @@ STR_NO_FOOTNOTES: "Бұл бетте түсіндірме жазбалар жо
|
|||||||
STR_LINK: "[сілтеме]"
|
STR_LINK: "[сілтеме]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
||||||
STR_SLEEP_NEVER: "Ешқашан"
|
STR_SLEEP_NEVER: "Ешқашан"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Сол/Оң: 1 мин Жоғары/Төмен: 5 мин"
|
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
|
||||||
|
STR_STEP_HINT_SIDE: "Бүйір түймелері:"
|
||||||
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
||||||
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
||||||
STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару"
|
STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Жасырын желіні қосу..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Желі атауын енгізіңіз (SSID)"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ STR_ORIENTATION: "Orientacija"
|
|||||||
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
|
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
|
||||||
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
|
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
|
||||||
STR_FONT_FAMILY: "Šriftas"
|
STR_FONT_FAMILY: "Šriftas"
|
||||||
STR_FONT_SIZE: "Šrifto dydis"
|
STR_FONT_SIZE: "Šrifto dydis"
|
||||||
STR_LINE_SPACING: "Tarpai tarp eilučių"
|
STR_LINE_SPACING: "Tarpai tarp eilučių"
|
||||||
@@ -93,6 +94,7 @@ STR_USERNAME: "Vartotojas"
|
|||||||
STR_PASSWORD: "Slaptažodis"
|
STR_PASSWORD: "Slaptažodis"
|
||||||
STR_SYNC_SERVER_URL: "Serverio URL"
|
STR_SYNC_SERVER_URL: "Serverio URL"
|
||||||
STR_DOCUMENT_MATCHING: "Atpažinimas"
|
STR_DOCUMENT_MATCHING: "Atpažinimas"
|
||||||
|
STR_SEND_METADATA: "Siųsti dokumento metaduomenis"
|
||||||
STR_AUTHENTICATE: "Prisijungti"
|
STR_AUTHENTICATE: "Prisijungti"
|
||||||
STR_KOREADER_USERNAME: "KOReader vartotojas"
|
STR_KOREADER_USERNAME: "KOReader vartotojas"
|
||||||
STR_KOREADER_PASSWORD: "KOReader slaptažodis"
|
STR_KOREADER_PASSWORD: "KOReader slaptažodis"
|
||||||
@@ -131,7 +133,8 @@ STR_SLEEP: "Miegas"
|
|||||||
STR_PAGE_TURN: "Versti psl."
|
STR_PAGE_TURN: "Versti psl."
|
||||||
STR_PORTRAIT: "Stačias"
|
STR_PORTRAIT: "Stačias"
|
||||||
STR_LANDSCAPE_CW: "Gulsčias (P)"
|
STR_LANDSCAPE_CW: "Gulsčias (P)"
|
||||||
STR_INVERTED: "Apverstas"
|
STR_INVERTED: "Invertuotas"
|
||||||
|
STR_ORIENTATION_INVERTED: "Stačias 180°"
|
||||||
STR_LANDSCAPE_CCW: "Gulsčias (A)"
|
STR_LANDSCAPE_CCW: "Gulsčias (A)"
|
||||||
STR_PREV_NEXT: "Atgal/Pirmyn"
|
STR_PREV_NEXT: "Atgal/Pirmyn"
|
||||||
STR_NEXT_PREV: "Pirmyn/Atgal"
|
STR_NEXT_PREV: "Pirmyn/Atgal"
|
||||||
@@ -183,6 +186,9 @@ STR_HOME: "« Pradžia"
|
|||||||
STR_SELECT: "Rinktis"
|
STR_SELECT: "Rinktis"
|
||||||
STR_SELECTED: "Pasirinkta"
|
STR_SELECTED: "Pasirinkta"
|
||||||
STR_TOGGLE: "Keisti"
|
STR_TOGGLE: "Keisti"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Perjungti žymę"
|
||||||
|
STR_BOOKMARK_REMOVED: "Žymė pašalinta."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Laikykite Atidaryti, kad ištrintumėte"
|
||||||
STR_CONFIRM: "Gerai"
|
STR_CONFIRM: "Gerai"
|
||||||
STR_CANCEL: "Atšaukti"
|
STR_CANCEL: "Atšaukti"
|
||||||
STR_CONNECT: "Jungtis"
|
STR_CONNECT: "Jungtis"
|
||||||
@@ -258,7 +264,6 @@ STR_CHAPTER_PREFIX: "Sk: "
|
|||||||
STR_PAGES_SEPARATOR: " psl. | "
|
STR_PAGES_SEPARATOR: " psl. | "
|
||||||
STR_BOOK_PREFIX: "Kn: "
|
STR_BOOK_PREFIX: "Kn: "
|
||||||
STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL"
|
STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL"
|
||||||
STR_PERCENT_STEP_HINT: "K/D: 1% | V/A: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sinchr. laikas..."
|
STR_SYNCING_TIME: "Sinchr. laikas..."
|
||||||
STR_CALC_HASH: "Skaičiuojama maiša..."
|
STR_CALC_HASH: "Skaičiuojama maiša..."
|
||||||
STR_HASH_FAILED: "Maišos klaida"
|
STR_HASH_FAILED: "Maišos klaida"
|
||||||
@@ -290,8 +295,11 @@ STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
|
|||||||
STR_LINK: "[nuoroda]"
|
STR_LINK: "[nuoroda]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
||||||
STR_SLEEP_NEVER: "Niekada"
|
STR_SLEEP_NEVER: "Niekada"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Kairė/Dešinė: 1 min. Viršus/Apačia: 5 min."
|
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
|
||||||
|
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
|
||||||
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
|
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
|
||||||
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
|
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
|
||||||
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
|
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Pridėti paslėptą tinklą..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Įveskite tinklo pavadinimą (SSID)"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Przeskocz rozdział"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Przeskocz rozdział"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientacja ekranu"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientacja ekranu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Pchnąć w tę łódź jeża lub ośm skrzyń fig"
|
||||||
STR_FONT_FAMILY: "Czcionka"
|
STR_FONT_FAMILY: "Czcionka"
|
||||||
STR_FONT_SIZE: "Rozmiar czcionki"
|
STR_FONT_SIZE: "Rozmiar czcionki"
|
||||||
STR_LINE_SPACING: "Odstępy między wierszami"
|
STR_LINE_SPACING: "Odstępy między wierszami"
|
||||||
@@ -96,6 +97,7 @@ STR_USERNAME: "Użytkownik"
|
|||||||
STR_PASSWORD: "Hasło"
|
STR_PASSWORD: "Hasło"
|
||||||
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
|
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
|
||||||
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
|
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
|
||||||
|
STR_SEND_METADATA: "Wyślij metadane dokumentu"
|
||||||
STR_AUTHENTICATE: "Uwierzytelnianie"
|
STR_AUTHENTICATE: "Uwierzytelnianie"
|
||||||
STR_KOREADER_USERNAME: "Użytkownik KOReader"
|
STR_KOREADER_USERNAME: "Użytkownik KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Hasło KOReader"
|
STR_KOREADER_PASSWORD: "Hasło KOReader"
|
||||||
@@ -135,7 +137,8 @@ STR_PAGE_TURN: "Nast. str."
|
|||||||
STR_FORCE_REFRESH: "Odśwież ekran"
|
STR_FORCE_REFRESH: "Odśwież ekran"
|
||||||
STR_PORTRAIT: "Pionowo"
|
STR_PORTRAIT: "Pionowo"
|
||||||
STR_LANDSCAPE_CW: "Poziomo P"
|
STR_LANDSCAPE_CW: "Poziomo P"
|
||||||
STR_INVERTED: "Odwrócony"
|
STR_INVERTED: "Inwersja"
|
||||||
|
STR_ORIENTATION_INVERTED: "Pionowo 180°"
|
||||||
STR_LANDSCAPE_CCW: "Poziomo L"
|
STR_LANDSCAPE_CCW: "Poziomo L"
|
||||||
STR_PREV_NEXT: "Poprz./Nast."
|
STR_PREV_NEXT: "Poprz./Nast."
|
||||||
STR_NEXT_PREV: "Nast./Poprz."
|
STR_NEXT_PREV: "Nast./Poprz."
|
||||||
@@ -190,6 +193,9 @@ STR_HOME: "« Home"
|
|||||||
STR_SELECT: "Wybierz"
|
STR_SELECT: "Wybierz"
|
||||||
STR_SELECTED: "Wybrano"
|
STR_SELECTED: "Wybrano"
|
||||||
STR_TOGGLE: "Zmień"
|
STR_TOGGLE: "Zmień"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Przełącz zakładkę"
|
||||||
|
STR_BOOKMARK_REMOVED: "Zakładka usunięta."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Przytrzymaj Otwórz, aby usunąć"
|
||||||
STR_CONFIRM: "Potwierdź"
|
STR_CONFIRM: "Potwierdź"
|
||||||
STR_CANCEL: "Anuluj"
|
STR_CANCEL: "Anuluj"
|
||||||
STR_CONNECT: "Połącz"
|
STR_CONNECT: "Połącz"
|
||||||
@@ -270,7 +276,6 @@ STR_CHAPTER_PREFIX: "Rozdział: "
|
|||||||
STR_PAGES_SEPARATOR: " stron | "
|
STR_PAGES_SEPARATOR: " stron | "
|
||||||
STR_BOOK_PREFIX: "Książka: "
|
STR_BOOK_PREFIX: "Książka: "
|
||||||
STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL"
|
STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL"
|
||||||
STR_PERCENT_STEP_HINT: "Lewo/Prawo: 1% Góra/Dół: 10%"
|
|
||||||
STR_SYNCING_TIME: "Synchronizacja czasu..."
|
STR_SYNCING_TIME: "Synchronizacja czasu..."
|
||||||
STR_CALC_HASH: "Obliczanie sumy kontrolnej..."
|
STR_CALC_HASH: "Obliczanie sumy kontrolnej..."
|
||||||
STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej"
|
STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej"
|
||||||
@@ -303,7 +308,8 @@ STR_NO_FOOTNOTES: "Brak przypisów na tej stronie"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nigdy"
|
STR_SLEEP_NEVER: "Nigdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Lewo/Prawo: 1 min Góra/Dół: 5 min"
|
STR_STEP_HINT_FRONT: "Przyciski przednie:"
|
||||||
|
STR_STEP_HINT_SIDE: "Przyciski boczne:"
|
||||||
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
|
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
|
||||||
STR_ADD_SERVER: "Dodaj serwer"
|
STR_ADD_SERVER: "Dodaj serwer"
|
||||||
STR_SERVER_NAME: "Nazwa serwera"
|
STR_SERVER_NAME: "Nazwa serwera"
|
||||||
@@ -355,3 +361,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
||||||
STR_RECOVERY_MODE: "Tryb przywracania"
|
STR_RECOVERY_MODE: "Tryb przywracania"
|
||||||
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
|
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Dodaj ukrytą sieć..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Wprowadź nazwę sieci (SSID)"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ STR_END_OF_BOOK: "Fim do livro"
|
|||||||
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||||
STR_INDEXING: "Indexando"
|
STR_INDEXING: "Indexando"
|
||||||
STR_MEMORY_ERROR: "Erro de memória"
|
STR_MEMORY_ERROR: "Erro de memória"
|
||||||
STR_PAGE_LOAD_ERROR: "Erro página"
|
STR_PAGE_LOAD_ERROR: "Erro ao carregar pág."
|
||||||
STR_EMPTY_FILE: "Arquivo vazio"
|
STR_EMPTY_FILE: "Arquivo vazio"
|
||||||
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||||
STR_LOADING: "Carregando..."
|
STR_LOADING: "Carregando..."
|
||||||
@@ -32,8 +32,8 @@ STR_CONNECTING: "Conectando..."
|
|||||||
STR_CONNECTED: "Conectado!"
|
STR_CONNECTED: "Conectado!"
|
||||||
STR_CONNECTION_FAILED: "Falha na conexão"
|
STR_CONNECTION_FAILED: "Falha na conexão"
|
||||||
STR_FORGET_NETWORK: "Esquecer rede?"
|
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||||
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
|
STR_SAVE_PASSWORD: "Salvar senha na próxima vez?"
|
||||||
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
|
STR_PRESS_OK_SCAN: "Pressione OK para buscar novamente"
|
||||||
STR_JOIN_NETWORK: "Entrar em uma rede"
|
STR_JOIN_NETWORK: "Entrar em uma rede"
|
||||||
STR_CREATE_HOTSPOT: "Criar hotspot"
|
STR_CREATE_HOTSPOT: "Criar hotspot"
|
||||||
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
|
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
|
||||||
@@ -41,7 +41,7 @@ STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas entrarem"
|
|||||||
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
||||||
STR_HOTSPOT_MODE: "Modo hotspot"
|
STR_HOTSPOT_MODE: "Modo hotspot"
|
||||||
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
|
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
|
||||||
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
|
STR_OPEN_URL_HINT: "Abra esta URL no seu navegador"
|
||||||
STR_OR_HTTP_PREFIX: "ou http://"
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
||||||
@@ -50,29 +50,36 @@ STR_MAC_ADDRESS: "Endereço MAC:"
|
|||||||
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
|
||||||
STR_TO_PREFIX: "para"
|
STR_TO_PREFIX: "para"
|
||||||
STR_CALIBRE_RECEIVING: "Recebendo:"
|
STR_CALIBRE_RECEIVING: "Recebendo: "
|
||||||
STR_CALIBRE_RECEIVED: "Recebido:"
|
STR_CALIBRE_RECEIVED: "Recebido: "
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
||||||
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
|
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
|
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar ao dispositivo\""
|
||||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
|
||||||
STR_CAT_DISPLAY: "Tela"
|
STR_CAT_DISPLAY: "Tela"
|
||||||
STR_CAT_READER: "Leitor"
|
STR_CAT_READER: "Leitor"
|
||||||
STR_CAT_CONTROLS: "Controles"
|
STR_CAT_CONTROLS: "Controles"
|
||||||
STR_CAT_SYSTEM: "Sistema"
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
STR_SLEEP_SCREEN: "Tela de repouso"
|
STR_SLEEP_SCREEN: "Tela de repouso"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
|
||||||
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
|
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
|
||||||
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||||
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
|
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
|
||||||
STR_TEXT_AA: "Suavização de texto"
|
STR_TEXT_AA: "Suavização de texto"
|
||||||
|
STR_IMAGES: "Imagens"
|
||||||
|
STR_IMAGES_DISPLAY: "Exibição"
|
||||||
|
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
|
||||||
|
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||||
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
||||||
STR_ORIENTATION: "Orientação de leitura"
|
STR_ORIENTATION: "Orientação de leitura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
|
STR_LONG_PRESS_BEHAVIOR: "Comportamento de Pressionar e segurar"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Pular capítulo"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
||||||
|
STR_LONG_PRESS_MENU: "Pressionar e segurar Menu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Um pequeno jabuti xereta viu dez cegonhas felizes. "
|
||||||
STR_FONT_FAMILY: "Fonte do leitor"
|
STR_FONT_FAMILY: "Fonte do leitor"
|
||||||
STR_FONT_SIZE: "Tam. fonte UI"
|
STR_FONT_SIZE: "Tam. fonte UI"
|
||||||
STR_LINE_SPACING: "Espaçamento entre linhas"
|
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||||
@@ -80,6 +87,7 @@ STR_SCREEN_MARGIN: "Margens da tela"
|
|||||||
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
|
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
|
||||||
STR_HYPHENATION: "Hifenização"
|
STR_HYPHENATION: "Hifenização"
|
||||||
STR_TIME_TO_SLEEP: "Tempo para repousar"
|
STR_TIME_TO_SLEEP: "Tempo para repousar"
|
||||||
|
STR_SHOW_HIDDEN_FILES: "Mostrar arquivos ocultos"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read"
|
STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read"
|
||||||
STR_REFRESH_FREQ: "Frequência atualização"
|
STR_REFRESH_FREQ: "Frequência atualização"
|
||||||
@@ -91,6 +99,7 @@ STR_USERNAME: "Nome de usuário"
|
|||||||
STR_PASSWORD: "Senha"
|
STR_PASSWORD: "Senha"
|
||||||
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
||||||
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
||||||
|
STR_SEND_METADATA: "Enviar metadados do doc."
|
||||||
STR_AUTHENTICATE: "Autenticar"
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
||||||
@@ -127,12 +136,17 @@ STR_ALWAYS: "Sempre"
|
|||||||
STR_IGNORE: "Ignorar"
|
STR_IGNORE: "Ignorar"
|
||||||
STR_SLEEP: "Repouso"
|
STR_SLEEP: "Repouso"
|
||||||
STR_PAGE_TURN: "Virar página"
|
STR_PAGE_TURN: "Virar página"
|
||||||
|
STR_FORCE_REFRESH: "Atualizar tela"
|
||||||
STR_PORTRAIT: "Retrato"
|
STR_PORTRAIT: "Retrato"
|
||||||
STR_LANDSCAPE_CW: "Paisagem H"
|
STR_LANDSCAPE_CW: "Paisagem H"
|
||||||
STR_INVERTED: "Invertido"
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_ORIENTATION_INVERTED: "Retrato 180°"
|
||||||
STR_LANDSCAPE_CCW: "Paisagem AH"
|
STR_LANDSCAPE_CCW: "Paisagem AH"
|
||||||
STR_PREV_NEXT: "Ant/Próx"
|
STR_PREV_NEXT: "Ant/Próx"
|
||||||
STR_NEXT_PREV: "Próx/Ant"
|
STR_NEXT_PREV: "Próx/Ant"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Marcador"
|
||||||
|
STR_DISABLED: "Desabilitado"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Pequeno"
|
STR_SMALL: "Pequeno"
|
||||||
@@ -160,17 +174,21 @@ STR_UPDATING: "Atualizando..."
|
|||||||
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
||||||
STR_UPDATE_FAILED: "Falha na atualização"
|
STR_UPDATE_FAILED: "Falha na atualização"
|
||||||
STR_UPDATE_COMPLETE: "Atualização concluída"
|
STR_UPDATE_COMPLETE: "Atualização concluída"
|
||||||
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente"
|
STR_POWER_ON_HINT: "Pressione e segure o botão de energia para ligar novamente"
|
||||||
STR_NO_ENTRIES: "Nenhum entries encontrado"
|
STR_RESTARTING_HINT: "Reiniciando... Se não reiniciar, pressione e segure o botão de energia por alguns segundos."
|
||||||
|
STR_NO_ENTRIES: "Nenhuma entrada encontrada"
|
||||||
STR_DOWNLOADING: "Baixando..."
|
STR_DOWNLOADING: "Baixando..."
|
||||||
STR_DOWNLOAD_FAILED: "Falha no download"
|
STR_DOWNLOAD_FAILED: "Falha no download"
|
||||||
STR_ERROR_MSG: "Erro:"
|
STR_ERROR_MSG: "Erro:"
|
||||||
STR_UNNAMED: "Sem nome"
|
STR_UNNAMED: "Sem nome"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Segure 'Abrir' para excluir"
|
||||||
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
|
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
|
||||||
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
|
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
|
||||||
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
|
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
|
||||||
STR_NETWORK_PREFIX: "Rede:"
|
STR_NEXT_PAGE: "Próxima Página »"
|
||||||
STR_IP_ADDRESS_PREFIX: "Endereço IP:"
|
STR_PREV_PAGE: "« Página Anterior"
|
||||||
|
STR_NETWORK_PREFIX: "Rede: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Endereço IP: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
||||||
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
||||||
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
|
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
|
||||||
@@ -179,7 +197,9 @@ STR_BACK: "« Voltar"
|
|||||||
STR_EXIT: "« Sair"
|
STR_EXIT: "« Sair"
|
||||||
STR_HOME: "« Início"
|
STR_HOME: "« Início"
|
||||||
STR_SELECT: "Escolher"
|
STR_SELECT: "Escolher"
|
||||||
|
STR_SELECTED: "Selecionado"
|
||||||
STR_TOGGLE: "Alternar"
|
STR_TOGGLE: "Alternar"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
||||||
STR_CONFIRM: "Confirmar"
|
STR_CONFIRM: "Confirmar"
|
||||||
STR_CANCEL: "Cancelar"
|
STR_CANCEL: "Cancelar"
|
||||||
STR_CONNECT: "Conectar"
|
STR_CONNECT: "Conectar"
|
||||||
@@ -188,6 +208,8 @@ STR_DOWNLOAD: "Baixar"
|
|||||||
STR_RETRY: "Tentar novamente"
|
STR_RETRY: "Tentar novamente"
|
||||||
STR_YES: "Sim"
|
STR_YES: "Sim"
|
||||||
STR_NO: "Não"
|
STR_NO: "Não"
|
||||||
|
STR_SHOW: "Mostrar"
|
||||||
|
STR_HIDE: "Ocultar"
|
||||||
STR_STATE_ON: "LIG."
|
STR_STATE_ON: "LIG."
|
||||||
STR_STATE_OFF: "DESL."
|
STR_STATE_OFF: "DESL."
|
||||||
STR_NOT_SET: "Não definido"
|
STR_NOT_SET: "Não definido"
|
||||||
@@ -198,15 +220,51 @@ STR_DIR_DOWN: "Baixo"
|
|||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
|
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
|
||||||
STR_FILTER_CONTRAST: "Contraste"
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
STR_UI_THEME: "Tema da interface"
|
STR_CUSTOMISE_STATUS_BAR: "Customizar Barra de Status"
|
||||||
|
STR_CHAPTER_PAGE_COUNT: "Páginas do Capítulo"
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE: "Porcentagem de Progresso"
|
||||||
|
STR_PROGRESS_BAR: "Barra de Progresso"
|
||||||
|
STR_PROGRESS_BAR_THICKNESS: "Espessura da Barra de Progresso"
|
||||||
|
STR_PROGRESS_BAR_THIN: "Fino"
|
||||||
|
STR_PROGRESS_BAR_MEDIUM: "Médio"
|
||||||
|
STR_PROGRESS_BAR_THICK: "Grosso"
|
||||||
|
STR_BOOK: "Livro"
|
||||||
|
STR_CHAPTER: "Capítulo"
|
||||||
|
STR_EXAMPLE_CHAPTER: "Capítulo 21"
|
||||||
|
STR_EXAMPLE_BOOK: "Título do Livro"
|
||||||
|
STR_PREVIEW: "Prévia"
|
||||||
|
STR_TITLE: "Título"
|
||||||
|
STR_BATTERY: "Bateria"
|
||||||
|
STR_XTC_STATUS_BAR: "Barra de Status XTC"
|
||||||
|
STR_BOTTOM: "Baixo"
|
||||||
|
STR_TOP: "Topo"
|
||||||
|
STR_CLOCK: "Relógio"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Offset UTC do Relógio"
|
||||||
|
STR_CLOCK_FORMAT: "Formato do Relógio"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 horas"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 horas"
|
||||||
|
STR_CURRENT_TIME: "Hora atual:"
|
||||||
|
STR_NEXT_FIELD: "Próximo"
|
||||||
|
STR_CLOCK_SYNC: "Sincronizar Relógio"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
|
||||||
|
STR_CLOCK_SYNCING: "Sincronizando via NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "Relógio sincronizado"
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Falha na sincronização"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não conectado"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Conecte ao Wi-Fi primeiro, depois tente novamente."
|
||||||
|
STR_CLOCK_SYNCED: "Relógio sincronizado"
|
||||||
|
STR_UI_THEME: "Tema"
|
||||||
STR_THEME_CLASSIC: "Clássico"
|
STR_THEME_CLASSIC: "Clássico"
|
||||||
STR_THEME_LYRA: "Lyra"
|
STR_THEME_LYRA: "Lyra"
|
||||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
STR_THEME_LYRA_EXTENDED: "Lyra Estendido"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
|
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
|
|
||||||
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
||||||
|
STR_BOOKMARKS: "Marcadores"
|
||||||
|
STR_BOOKMARK_ADDED: "Marcador adicionado."
|
||||||
|
STR_BOOKMARK_REMOVED: "Marcador removido."
|
||||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
|
STR_SEARCH: "Busca"
|
||||||
STR_COVER_CUSTOM: "Capa + personalizado"
|
STR_COVER_CUSTOM: "Capa + personalizado"
|
||||||
STR_QUICK_RESUME: "Retomada rápida"
|
STR_QUICK_RESUME: "Retomada rápida"
|
||||||
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||||
@@ -220,11 +278,11 @@ STR_CALIBRE_SETUP: "Configuração"
|
|||||||
STR_CALIBRE_STATUS: "Status"
|
STR_CALIBRE_STATUS: "Status"
|
||||||
STR_CLEAR_BUTTON: "Limpar"
|
STR_CLEAR_BUTTON: "Limpar"
|
||||||
STR_DEFAULT_VALUE: "Padrão"
|
STR_DEFAULT_VALUE: "Padrão"
|
||||||
STR_REMAP_PROMPT: "Pressione um botão frontal cada função"
|
STR_REMAP_PROMPT: "Pressione um botão frontal para cada função"
|
||||||
STR_UNASSIGNED: "Não atribuído"
|
STR_UNASSIGNED: "Não atribuído"
|
||||||
STR_ALREADY_ASSIGNED: "Já atribuído"
|
STR_ALREADY_ASSIGNED: "Já atribuído"
|
||||||
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão"
|
STR_REMAP_RESET_HINT: "Botão lateral de cima: redefinir layout padrão"
|
||||||
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento"
|
STR_REMAP_CANCEL_HINT: "Botão lateral de baixo: cancelar remapeamento"
|
||||||
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
||||||
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
||||||
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
||||||
@@ -234,14 +292,15 @@ STR_GO_HOME_BUTTON: "Ir para o início"
|
|||||||
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||||
STR_DELETE_CACHE: "Excluir cache do livro"
|
STR_DELETE_CACHE: "Excluir cache do livro"
|
||||||
STR_DELETE: "Excluir"
|
STR_DELETE: "Excluir"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Excluir este marcador?"
|
||||||
|
STR_DISPLAY_QR: "Mostrar página como QR"
|
||||||
STR_CHAPTER_PREFIX: "Capítulo:"
|
STR_CHAPTER_PREFIX: "Capítulo:"
|
||||||
STR_PAGES_SEPARATOR: "páginas |"
|
STR_PAGES_SEPARATOR: "páginas |"
|
||||||
STR_BOOK_PREFIX: "Livro:"
|
STR_BOOK_PREFIX: "Livro:"
|
||||||
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
||||||
STR_PERCENT_STEP_HINT: "Esq/Dir: 1% Cima/Baixo: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sincronizando horário..."
|
STR_SYNCING_TIME: "Sincronizando horário..."
|
||||||
STR_CALC_HASH: "Calculando hash documento..."
|
STR_CALC_HASH: "Calculando hash do documento..."
|
||||||
STR_HASH_FAILED: "Falha ao calcular o hash documento"
|
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
|
||||||
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
|
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
|
||||||
STR_UPLOAD_PROGRESS: "Enviando progresso..."
|
STR_UPLOAD_PROGRESS: "Enviando progresso..."
|
||||||
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||||
@@ -259,14 +318,71 @@ STR_UPLOAD_PROMPT: "Enviar posição atual?"
|
|||||||
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
||||||
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
||||||
STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso"
|
STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso"
|
||||||
STR_SECTION_PREFIX: "Seção"
|
STR_SECTION_PREFIX: "Seção "
|
||||||
STR_UPLOAD: "Enviar"
|
STR_UPLOAD: "Enviar"
|
||||||
STR_BOOK_S_STYLE: "Estilo do livro"
|
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||||
STR_EMBEDDED_STYLE: "Estilo embutido"
|
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||||
STR_FOCUS_READING: "Leitura focada"
|
STR_FOCUS_READING: "Leitura focada"
|
||||||
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido de notas de rodapé"
|
||||||
|
STR_SET_SLEEP_COVER: "Definir capa"
|
||||||
|
STR_FOOTNOTES: "Notas de rodapé"
|
||||||
|
STR_NO_FOOTNOTES: "Nenhuma nota de rodapé nesta página"
|
||||||
|
STR_LINK: "[link]"
|
||||||
STR_SCREENSHOT_BUTTON: "Capturar tela"
|
STR_SCREENSHOT_BUTTON: "Capturar tela"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nunca"
|
STR_SLEEP_NEVER: "Nunca"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Esq/Dir: 1 min Cima/Baixo: 5 min"
|
STR_STEP_HINT_FRONT: "Botões frontais:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botões laterais:"
|
||||||
|
STR_ADD_SERVER: "Adicionar Servidor"
|
||||||
|
STR_SERVER_NAME: "Nome do Servidor"
|
||||||
|
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
|
||||||
|
STR_DELETE_SERVER: "Excluir Servidor"
|
||||||
|
STR_OPDS_SERVERS: "Servidores OPDS"
|
||||||
|
STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
|
||||||
|
STR_MANAGE_FONTS: "Gerenciar fontes"
|
||||||
|
STR_FONT_BROWSER: "Navegador de fontes"
|
||||||
|
STR_LOADING_FONT_LIST: "Carregando lista de fontes..."
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível"
|
||||||
|
STR_FONT_INSTALLED: "Fonte instalada!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Falha na instalação da fonte"
|
||||||
|
STR_INSTALLED: "Instalada"
|
||||||
|
STR_DOWNLOAD_ALL: "Baixar tudo"
|
||||||
|
STR_UPDATE_ALL: "Atualizar tudo"
|
||||||
|
STR_UPDATE_AVAILABLE: "Atualizar"
|
||||||
|
STR_CRASH_TITLE: "Falha do sistema"
|
||||||
|
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi salvo em crash_report.txt. Por favor, inclua este arquivo no seu relatório de erro."
|
||||||
|
STR_CRASH_REASON: "Motivo da falha:"
|
||||||
|
STR_CRASH_NO_REASON: "(Nenhum motivo registrado)"
|
||||||
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Pressione ESQ ou DIR para mover o cursor"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Pressione ESQ para retornar à posição do cursor"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Segure DIR e pressione [***] para ocultar a senha"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Segure DIR e pressione [abc] para exibir a senha"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pressione [***] para ocultar a senha"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pressione [abc] para exibir a senha"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Segure CIMA para editar a entrada"
|
||||||
|
STR_KB_TIPS: "Dicas:"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Pressione BAIXO para retornar ao teclado"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Pressione ABC para sair do modo URL"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Segure DEL para limpar todo o texto"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Segure SELECT para caractere secundário"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Segure SELECT para MAIÚSCULO ou caractere secundário"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Segure SELECT para minúsculo ou caractere secundário"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Pressione URL para atalhos"
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Selecione o arquivo de firmware (.bin)"
|
||||||
|
STR_NO_BIN_FILES: "Nenhum arquivo .bin encontrado"
|
||||||
|
STR_VALIDATING_FIRMWARE: "Validando firmware..."
|
||||||
|
STR_INVALID_FIRMWARE: "Arquivo de firmware inválido"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "Firmware grande demais para a partição"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "Arquivo de firmware pequeno demais"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o arquivo"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
|
||||||
|
STR_RECOVERY_MODE: "Modo de Recuperação"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Coloque o arquivo firmware.bin na raiz do cartão SD e selecione-o"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Insira o nome da rede (SSID)"
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
_language_name: "Português (Portugal)"
|
||||||
|
_language_code: "P2"
|
||||||
|
_order: "27"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "A INICIAR"
|
||||||
|
STR_SLEEPING: "EM REPOUSO"
|
||||||
|
STR_ENTERING_SLEEP: "A entrar em repouso"
|
||||||
|
STR_BROWSE_FILES: "Explorar ficheiros"
|
||||||
|
STR_FILE_TRANSFER: "Transferência de ficheiros"
|
||||||
|
STR_SETTINGS_TITLE: "Definições"
|
||||||
|
STR_CONTINUE_READING: "Continuar a ler"
|
||||||
|
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
|
||||||
|
STR_START_READING: "Comece a ler abaixo"
|
||||||
|
STR_NO_FILES_FOUND: "Nenhum ficheiro encontrado"
|
||||||
|
STR_SELECT_CHAPTER: "Selecionar capítulo"
|
||||||
|
STR_NO_CHAPTERS: "Sem capítulos"
|
||||||
|
STR_END_OF_BOOK: "Fim do livro"
|
||||||
|
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||||
|
STR_INDEXING: "A indexar"
|
||||||
|
STR_INDEX_FAILED: "Falha ao indexar - livro inválido"
|
||||||
|
STR_MEMORY_ERROR: "Erro de memória"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Erro ao carregar a página"
|
||||||
|
STR_EMPTY_FILE: "Ficheiro vazio"
|
||||||
|
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||||
|
STR_LOADING: "A carregar..."
|
||||||
|
STR_LOADING_POPUP: "A carregar"
|
||||||
|
STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||||
|
STR_NO_NETWORKS: "Nenhuma rede encontrada"
|
||||||
|
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||||
|
STR_SCANNING: "A procurar..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "A procurar Wi-Fi guardado..."
|
||||||
|
STR_CONNECTING: "A ligar..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "A ligar ao Wi-Fi guardado..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostrar"
|
||||||
|
STR_CONNECTED: "Ligado!"
|
||||||
|
STR_CONNECTION_FAILED: "Falha na ligação"
|
||||||
|
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||||
|
STR_SAVE_PASSWORD: "Guardar palavra-passe para a próxima vez?"
|
||||||
|
STR_PRESS_OK_SCAN: "Prima OK para procurar novamente"
|
||||||
|
STR_JOIN_NETWORK: "Aderir a uma rede"
|
||||||
|
STR_CREATE_HOTSPOT: "Criar ponto de acesso"
|
||||||
|
STR_JOIN_DESC: "Ligue-se a uma rede Wi-Fi existente"
|
||||||
|
STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas se ligarem"
|
||||||
|
STR_STARTING_HOTSPOT: "A iniciar ponto de acesso..."
|
||||||
|
STR_HOTSPOT_MODE: "Modo de ponto de acesso"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Ligue o seu dispositivo a esta rede Wi-Fi"
|
||||||
|
STR_OPEN_URL_HINT: "Abra este URL no seu navegador"
|
||||||
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
|
STR_SCAN_QR_HINT: "ou leia o código QR com o seu telemóvel:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre sem fios"
|
||||||
|
STR_NETWORK_LEGEND: "* = Encriptada | + = Guardada"
|
||||||
|
STR_MAC_ADDRESS: "Endereço MAC:"
|
||||||
|
STR_CHECKING_WIFI: "A verificar o Wi-Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Introduza a palavra-passe do Wi-Fi"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduza o nome da rede (SSID)"
|
||||||
|
STR_TO_PREFIX: "para "
|
||||||
|
STR_CALIBRE_RECEIVING: "A receber: "
|
||||||
|
STR_CALIBRE_RECEIVED: "Recebido: "
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar para o dispositivo\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha este ecrã aberto durante o envio\""
|
||||||
|
STR_CAT_DISPLAY: "Ecrã"
|
||||||
|
STR_CAT_READER: "Leitor"
|
||||||
|
STR_CAT_CONTROLS: "Controlos"
|
||||||
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
|
STR_SLEEP_SCREEN: "Ecrã de repouso"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "Retoma rápida após tempo limite"
|
||||||
|
STR_SLEEP_COVER_MODE: "Modo de capa no ecrã de repouso"
|
||||||
|
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||||
|
STR_EXTRA_SPACING: "Espaçamento extra entre parágrafos"
|
||||||
|
STR_TEXT_AA: "Suavização do texto"
|
||||||
|
STR_IMAGES: "Imagens"
|
||||||
|
STR_IMAGES_DISPLAY: "Exibição"
|
||||||
|
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
|
||||||
|
STR_IMAGES_SUPPRESS: "Suprimir"
|
||||||
|
STR_EOB_HOME: "Início"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continuar com"
|
||||||
|
STR_SHORT_PWR_BTN: "Clique curto no botão de energia"
|
||||||
|
STR_ORIENTATION: "Orientação de leitura"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais (leitor)"
|
||||||
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR: "Comportamento de pressão longa"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
||||||
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
||||||
|
STR_LONG_PRESS_MENU: "Pressão longa no Menu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "A rápida raposa castanha salta por cima do cão preguiçoso"
|
||||||
|
STR_FONT_FAMILY: "Tipo de letra do leitor"
|
||||||
|
STR_FONT_SIZE: "Tamanho do tipo de letra"
|
||||||
|
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||||
|
STR_SCREEN_MARGIN: "Margens do ecrã"
|
||||||
|
STR_PARA_ALIGNMENT: "Alinhamento dos parágrafos"
|
||||||
|
STR_HYPHENATION: "Hifenização"
|
||||||
|
STR_TIME_TO_SLEEP: "Tempo até repousar"
|
||||||
|
STR_SHOW_HIDDEN_FILES: "Mostrar ficheiros ocultos"
|
||||||
|
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
||||||
|
STR_MOVE_FINISHED_TO_READ: "Mover livros concluídos para a pasta Read"
|
||||||
|
STR_REFRESH_FREQ: "Frequência de atualização"
|
||||||
|
STR_KOREADER_SYNC: "Sincronização KOReader"
|
||||||
|
STR_CHECK_UPDATES: "Procurar atualizações"
|
||||||
|
STR_LANGUAGE: "Idioma"
|
||||||
|
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
|
||||||
|
STR_USERNAME: "Nome de utilizador"
|
||||||
|
STR_PASSWORD: "Palavra-passe"
|
||||||
|
STR_SYNC_SERVER_URL: "URL do servidor de sincronização"
|
||||||
|
STR_DOCUMENT_MATCHING: "Correspondência de documentos"
|
||||||
|
STR_SEND_METADATA: "Enviar metadados do documento"
|
||||||
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
|
STR_KOREADER_USERNAME: "Utilizador do KOReader"
|
||||||
|
STR_KOREADER_PASSWORD: "Palavra-passe do KOReader"
|
||||||
|
STR_FILENAME: "Nome do ficheiro"
|
||||||
|
STR_BINARY: "Binário"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
|
||||||
|
STR_WIFI_CONN_FAILED: "Falha na ligação Wi-Fi"
|
||||||
|
STR_AUTHENTICATING: "A autenticar..."
|
||||||
|
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
|
||||||
|
STR_KOREADER_AUTH: "Autenticação KOReader"
|
||||||
|
STR_SYNC_READY: "A sincronização KOReader está pronta a usar"
|
||||||
|
STR_AUTH_FAILED: "Falha na autenticação"
|
||||||
|
STR_DONE: "Concluído"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Isto irá limpar todos os dados de livros em cache."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Os livros terão de ser reindexados"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
|
||||||
|
STR_CLEARING_CACHE: "A limpar a cache..."
|
||||||
|
STR_CACHE_CLEARED: "Cache limpa"
|
||||||
|
STR_ITEMS_REMOVED: "itens removidos"
|
||||||
|
STR_FAILED_LOWER: "falhou"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Falha ao limpar a cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Verifique a saída de série para obter detalhes"
|
||||||
|
STR_DARK: "Escuro"
|
||||||
|
STR_LIGHT: "Claro"
|
||||||
|
STR_CUSTOM: "Personalizado"
|
||||||
|
STR_COVER: "Capa"
|
||||||
|
STR_NONE_OPT: "Nenhum"
|
||||||
|
STR_FIT: "Ajustar"
|
||||||
|
STR_CROP: "Recortar"
|
||||||
|
STR_NEVER: "Nunca"
|
||||||
|
STR_IN_READER: "No leitor"
|
||||||
|
STR_ALWAYS: "Sempre"
|
||||||
|
STR_IGNORE: "Ignorar"
|
||||||
|
STR_SLEEP: "Repouso"
|
||||||
|
STR_PAGE_TURN: "Virar página"
|
||||||
|
STR_FORCE_REFRESH: "Atualizar ecrã"
|
||||||
|
STR_PORTRAIT: "Retrato"
|
||||||
|
STR_LANDSCAPE_CW: "Paisagem (Dir.)"
|
||||||
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_ORIENTATION_INVERTED: "Retrato 180°"
|
||||||
|
STR_LANDSCAPE_CCW: "Paisagem (Esq.)"
|
||||||
|
STR_PREV_NEXT: "Ant/Seg"
|
||||||
|
STR_NEXT_PREV: "Seg/Ant"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Marcador"
|
||||||
|
STR_DISABLED: "Desativado"
|
||||||
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_SMALL: "Pequeno"
|
||||||
|
STR_MEDIUM: "Médio"
|
||||||
|
STR_LARGE: "Grande"
|
||||||
|
STR_X_LARGE: "Extra grande"
|
||||||
|
STR_TIGHT: "Apertado"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Largo"
|
||||||
|
STR_JUSTIFY: "Justificar"
|
||||||
|
STR_ALIGN_LEFT: "Esquerda"
|
||||||
|
STR_CENTER: "Centrar"
|
||||||
|
STR_ALIGN_RIGHT: "Direita"
|
||||||
|
STR_PAGES_1: "1 página"
|
||||||
|
STR_PAGES_5: "5 páginas"
|
||||||
|
STR_PAGES_10: "10 páginas"
|
||||||
|
STR_PAGES_15: "15 páginas"
|
||||||
|
STR_PAGES_30: "30 páginas"
|
||||||
|
STR_UPDATE: "Atualizar"
|
||||||
|
STR_CHECKING_UPDATE: "A procurar atualização..."
|
||||||
|
STR_NEW_UPDATE: "Nova atualização disponível!"
|
||||||
|
STR_CURRENT_VERSION: "Versão atual: "
|
||||||
|
STR_NEW_VERSION: "Nova versão: "
|
||||||
|
STR_UPDATING: "A atualizar..."
|
||||||
|
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
||||||
|
STR_UPDATE_FAILED: "Falha na atualização"
|
||||||
|
STR_UPDATE_COMPLETE: "Atualização concluída"
|
||||||
|
STR_POWER_ON_HINT: "Prima sem soltar o botão de energia para voltar a ligar"
|
||||||
|
STR_RESTARTING_HINT: "A reiniciar... Se o dispositivo não reiniciar, mantenha premido o botão de energia durante alguns segundos."
|
||||||
|
STR_NO_ENTRIES: "Nenhum registo encontrado"
|
||||||
|
STR_DOWNLOADING: "A transferir..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Falha na transferência"
|
||||||
|
STR_ERROR_MSG: "Erro:"
|
||||||
|
STR_UNNAMED: "Sem nome"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Mantenha premido Abrir para eliminar"
|
||||||
|
STR_NO_SERVER_URL: "Nenhum URL de servidor configurado"
|
||||||
|
STR_FETCH_FEED_FAILED: "Falha ao obter o feed"
|
||||||
|
STR_PARSE_FEED_FAILED: "Falha ao analisar o feed"
|
||||||
|
STR_NEXT_PAGE: "Página seguinte »"
|
||||||
|
STR_PREV_PAGE: "« Página anterior"
|
||||||
|
STR_NETWORK_PREFIX: "Rede: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Endereço IP: "
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite de ligação esgotado"
|
||||||
|
STR_SD_CARD: "Cartão SD"
|
||||||
|
STR_BACK: "« Voltar"
|
||||||
|
STR_EXIT: "« Sair"
|
||||||
|
STR_HOME: "« Início"
|
||||||
|
STR_SELECT: "Selecionar"
|
||||||
|
STR_SELECTED: "Selecionado"
|
||||||
|
STR_TOGGLE: "Alternar"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
||||||
|
STR_CONFIRM: "Confirmar"
|
||||||
|
STR_CANCEL: "Cancelar"
|
||||||
|
STR_CONNECT: "Ligar"
|
||||||
|
STR_OPEN: "Abrir"
|
||||||
|
STR_DOWNLOAD: "Transferir"
|
||||||
|
STR_RETRY: "Tentar novamente"
|
||||||
|
STR_YES: "Sim"
|
||||||
|
STR_NO: "Não"
|
||||||
|
STR_SHOW: "Mostrar"
|
||||||
|
STR_HIDE: "Ocultar"
|
||||||
|
STR_STATE_ON: "LIG."
|
||||||
|
STR_STATE_OFF: "DESL."
|
||||||
|
STR_NOT_SET: "Não definido"
|
||||||
|
STR_DIR_LEFT: "Esquerda"
|
||||||
|
STR_DIR_RIGHT: "Direita"
|
||||||
|
STR_DIR_UP: "Cima"
|
||||||
|
STR_DIR_DOWN: "Baixo"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Filtro da capa de ecrã de repouso"
|
||||||
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
|
STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado"
|
||||||
|
STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo"
|
||||||
|
STR_BOOK_PROGRESS_PERCENTAGE: "Percentagem de progresso do livro"
|
||||||
|
STR_PROGRESS_BAR: "Barra de progresso"
|
||||||
|
STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso"
|
||||||
|
STR_PROGRESS_BAR_THIN: "Fina"
|
||||||
|
STR_PROGRESS_BAR_MEDIUM: "Média"
|
||||||
|
STR_PROGRESS_BAR_THICK: "Grossa"
|
||||||
|
STR_BOOK: "Livro"
|
||||||
|
STR_CHAPTER: "Capítulo"
|
||||||
|
STR_EXAMPLE_CHAPTER: "Capítulo 21"
|
||||||
|
STR_EXAMPLE_BOOK: "Título do livro"
|
||||||
|
STR_PREVIEW: "Pré-visualização"
|
||||||
|
STR_TITLE: "Título"
|
||||||
|
STR_BATTERY: "Bateria"
|
||||||
|
STR_XTC_STATUS_BAR: "Barra de estado XTC"
|
||||||
|
STR_BOTTOM: "Inferior"
|
||||||
|
STR_TOP: "Superior"
|
||||||
|
STR_CLOCK: "Relógio"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Deslocamento UTC do relógio"
|
||||||
|
STR_CLOCK_FORMAT: "Formato do relógio"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24 horas"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12 horas"
|
||||||
|
STR_CURRENT_TIME: "Hora atual:"
|
||||||
|
STR_NEXT_FIELD: "Seguinte"
|
||||||
|
STR_CLOCK_SYNC: "Sincronizar relógio"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
|
||||||
|
STR_CLOCK_SYNCING: "A sincronizar via NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "Relógio sincronizado"
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Falha na sincronização"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não ligado"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Ligue-se primeiro ao Wi-Fi e, em seguida, tente novamente."
|
||||||
|
STR_CLOCK_SYNCED: "Relógio sincronizado"
|
||||||
|
STR_UI_THEME: "Tema da interface"
|
||||||
|
STR_THEME_CLASSIC: "Clássico"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
|
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Ajuste de desbotamento ao sol"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
||||||
|
STR_BOOKMARKS: "Marcadores"
|
||||||
|
STR_BOOKMARK_ADDED: "Marcador adicionado."
|
||||||
|
STR_BOOKMARK_REMOVED: "Marcador removido."
|
||||||
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
|
STR_SEARCH: "Pesquisar"
|
||||||
|
STR_COVER_CUSTOM: "Capa + Personalizado"
|
||||||
|
STR_QUICK_RESUME: "Retoma rápida"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||||
|
STR_REMOVE_FROM_RECENTS: "Remover dos Livros recentes?"
|
||||||
|
STR_NO_RECENT_BOOKS: "Nenhum livro recente"
|
||||||
|
STR_CALIBRE_DESC: "Usar transferências sem fios de dispositivos do Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Esquecer rede e remover palavra-passe guardada?"
|
||||||
|
STR_FORGET_BUTTON: "Esquecer"
|
||||||
|
STR_CALIBRE_STARTING: "A iniciar o Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Configuração"
|
||||||
|
STR_CALIBRE_STATUS: "Estado"
|
||||||
|
STR_CLEAR_BUTTON: "Limpar"
|
||||||
|
STR_DEFAULT_VALUE: "Predefinição"
|
||||||
|
STR_REMAP_PROMPT: "Prima um botão frontal para cada função"
|
||||||
|
STR_UNASSIGNED: "Não atribuído"
|
||||||
|
STR_ALREADY_ASSIGNED: "Já atribuído"
|
||||||
|
STR_REMAP_RESET_HINT: "Botão lateral Cima: Repor disposição predefinida"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Botão lateral Baixo: Cancelar remapeamento"
|
||||||
|
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
||||||
|
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
|
||||||
|
STR_GO_TO_PERCENT: "Ir para %"
|
||||||
|
STR_GO_HOME_BUTTON: "Ir para o Início"
|
||||||
|
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||||
|
STR_DELETE_CACHE: "Eliminar cache do livro"
|
||||||
|
STR_DELETE: "Eliminar"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Eliminar este marcador?"
|
||||||
|
STR_DISPLAY_QR: "Mostrar página como QR"
|
||||||
|
STR_CHAPTER_PREFIX: "Capítulo: "
|
||||||
|
STR_PAGES_SEPARATOR: " páginas | "
|
||||||
|
STR_BOOK_PREFIX: "Livro: "
|
||||||
|
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
||||||
|
STR_SYNCING_TIME: "A sincronizar a hora..."
|
||||||
|
STR_CALC_HASH: "A calcular o hash do documento..."
|
||||||
|
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
|
||||||
|
STR_FETCH_PROGRESS: "A procurar progresso remoto..."
|
||||||
|
STR_UPLOAD_PROGRESS: "A enviar progresso..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader nas Definições"
|
||||||
|
STR_PROGRESS_FOUND: "Progresso encontrado!"
|
||||||
|
STR_REMOTE_LABEL: "Remoto:"
|
||||||
|
STR_LOCAL_LABEL: "Local:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||||
|
STR_APPLY_REMOTE: "Aplicar progresso remoto"
|
||||||
|
STR_UPLOAD_LOCAL: "Enviar progresso local"
|
||||||
|
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
|
||||||
|
STR_UPLOAD_PROMPT: "Enviar posição atual?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
||||||
|
STR_SAVE_PROGRESS_FAILED: "Não foi possível guardar o progresso"
|
||||||
|
STR_SECTION_PREFIX: "Secção "
|
||||||
|
STR_UPLOAD: "Enviar"
|
||||||
|
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||||
|
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||||
|
STR_FOCUS_READING: "Leitura focada"
|
||||||
|
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido das notas de rodapé"
|
||||||
|
STR_SET_SLEEP_COVER: "Definir capa"
|
||||||
|
STR_FOOTNOTES: "Notas de rodapé"
|
||||||
|
STR_NO_FOOTNOTES: "Nenhuma nota de rodapé nesta página"
|
||||||
|
STR_LINK: "[ligação]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "Captura de ecrã"
|
||||||
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
|
STR_SLEEP_NEVER: "Nunca"
|
||||||
|
STR_STEP_HINT_FRONT: "Botões frontais:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botões laterais:"
|
||||||
|
STR_ADD_SERVER: "Adicionar servidor"
|
||||||
|
STR_SERVER_NAME: "Nome do servidor"
|
||||||
|
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
|
||||||
|
STR_DELETE_SERVER: "Eliminar servidor"
|
||||||
|
STR_OPDS_SERVERS: "Servidores OPDS"
|
||||||
|
STR_AUTO_TURN_ENABLED: "Virar página automático ativado: "
|
||||||
|
STR_AUTO_TURN_PAGES_PER_MIN: "Virar página automático (Páginas por minuto)"
|
||||||
|
STR_MANAGE_FONTS: "Gerir tipos de letra"
|
||||||
|
STR_FONT_BROWSER: "Navegador de tipos de letra"
|
||||||
|
STR_LOADING_FONT_LIST: "A carregar a lista de tipos de letra..."
|
||||||
|
STR_NO_FONTS_AVAILABLE: "Nenhum tipo de letra disponível"
|
||||||
|
STR_FONT_INSTALLED: "Tipo de letra instalado!"
|
||||||
|
STR_FONT_INSTALL_FAILED: "Falha na instalação do tipo de letra"
|
||||||
|
STR_INSTALLED: "Instalado"
|
||||||
|
STR_DOWNLOAD_ALL: "Transferir tudo"
|
||||||
|
STR_UPDATE_ALL: "Atualizar tudo"
|
||||||
|
STR_UPDATE_AVAILABLE: "Atualizar"
|
||||||
|
STR_CRASH_TITLE: "Falha do sistema"
|
||||||
|
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi guardado em crash_report.txt. Por favor, inclua este ficheiro no seu relatório de erro."
|
||||||
|
STR_CRASH_REASON: "Motivo da falha:"
|
||||||
|
STR_CRASH_NO_REASON: "(Nenhum motivo foi registado)"
|
||||||
|
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
||||||
|
STR_KB_HINT_MOVE_CURSOR: "Prima ESQUERDA ou DIREITA para mover o cursor"
|
||||||
|
STR_KB_HINT_RETURN_CURSOR: "Prima ESQUERDA para regressar à posição do cursor"
|
||||||
|
STR_KB_HINT_HIDE_PASSWORD: "Mantenha premido DIREITA e depois prima [***] para ocultar a palavra-passe"
|
||||||
|
STR_KB_HINT_SHOW_PASSWORD: "Mantenha premido DIREITA e depois prima [abc] para mostrar a palavra-passe"
|
||||||
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prima [***] para ocultar a palavra-passe"
|
||||||
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prima [abc] para mostrar a palavra-passe"
|
||||||
|
STR_KB_HINT_EDIT_ENTRY: "Mantenha premido CIMA para editar a entrada"
|
||||||
|
STR_KB_TIPS: "Dicas:"
|
||||||
|
STR_KB_HINT_RETURN_KEYBOARD: "Prima BAIXO para regressar ao teclado"
|
||||||
|
STR_KB_HINT_EXIT_URL_MODE: "Prima ABC para sair do modo URL"
|
||||||
|
STR_KB_HINT_CLEAR_TEXT: "Mantenha premido DEL para limpar todo o texto"
|
||||||
|
STR_KB_HINT_SECONDARY_CHAR: "Mantenha premido SELECT para o carácter secundário"
|
||||||
|
STR_KB_HINT_UPPER_SECONDARY: "Mantenha premido SELECT para MAIÚSCULAS ou carácter secundário"
|
||||||
|
STR_KB_HINT_LOWER_SECONDARY: "Mantenha premido SELECT para minúsculas ou carácter secundário"
|
||||||
|
STR_KB_HINT_URL_SNIPPETS: "Prima URL para atalhos"
|
||||||
|
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
|
||||||
|
STR_SELECT_FIRMWARE_FILE: "Selecione o ficheiro de firmware (.bin)"
|
||||||
|
STR_NO_BIN_FILES: "Nenhum ficheiro .bin encontrado"
|
||||||
|
STR_VALIDATING_FIRMWARE: "A validar o firmware..."
|
||||||
|
STR_INVALID_FIRMWARE: "Ficheiro de firmware inválido"
|
||||||
|
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande para a partição"
|
||||||
|
STR_FIRMWARE_TOO_SMALL: "Ficheiro de firmware demasiado pequeno"
|
||||||
|
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
|
||||||
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o ficheiro"
|
||||||
|
STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
|
||||||
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
|
||||||
|
STR_RECOVERY_MODE: "Modo de Recuperação"
|
||||||
|
STR_RECOVERY_MODE_HINT: "Coloque o ficheiro firmware.bin na raiz do cartão SD e selecione-o"
|
||||||
@@ -77,6 +77,7 @@ STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Sărire capitol"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Sărire capitol"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Schimbă orientarea"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Schimbă orientarea"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Încă vând gem, whisky bej și tequila roz, preț fix"
|
||||||
STR_FONT_FAMILY: "Familie font lectură"
|
STR_FONT_FAMILY: "Familie font lectură"
|
||||||
STR_FONT_SIZE: "Dimensiune font"
|
STR_FONT_SIZE: "Dimensiune font"
|
||||||
STR_LINE_SPACING: "Spaţiere între rânduri"
|
STR_LINE_SPACING: "Spaţiere între rânduri"
|
||||||
@@ -96,6 +97,7 @@ STR_USERNAME: "Utilizator"
|
|||||||
STR_PASSWORD: "Parolă"
|
STR_PASSWORD: "Parolă"
|
||||||
STR_SYNC_SERVER_URL: "URL server sincronizare"
|
STR_SYNC_SERVER_URL: "URL server sincronizare"
|
||||||
STR_DOCUMENT_MATCHING: "Corespondenţă document"
|
STR_DOCUMENT_MATCHING: "Corespondenţă document"
|
||||||
|
STR_SEND_METADATA: "Trimite metadate document"
|
||||||
STR_AUTHENTICATE: "Autentificare"
|
STR_AUTHENTICATE: "Autentificare"
|
||||||
STR_KOREADER_USERNAME: "Nume utilizator KOReader"
|
STR_KOREADER_USERNAME: "Nume utilizator KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Parolă KOReader"
|
STR_KOREADER_PASSWORD: "Parolă KOReader"
|
||||||
@@ -135,6 +137,7 @@ STR_PAGE_TURN: "Răsfoire pagină"
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Orizontal dreapta"
|
STR_LANDSCAPE_CW: "Orizontal dreapta"
|
||||||
STR_INVERTED: "Invers"
|
STR_INVERTED: "Invers"
|
||||||
|
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||||
STR_LANDSCAPE_CCW: "Orizontal stânga"
|
STR_LANDSCAPE_CCW: "Orizontal stânga"
|
||||||
STR_PREV_NEXT: "Înainte/Înapoi"
|
STR_PREV_NEXT: "Înainte/Înapoi"
|
||||||
STR_NEXT_PREV: "Înapoi/Înainte"
|
STR_NEXT_PREV: "Înapoi/Înainte"
|
||||||
@@ -186,6 +189,9 @@ STR_HOME: "« Acasă"
|
|||||||
STR_SELECT: "Selectează"
|
STR_SELECT: "Selectează"
|
||||||
STR_SELECTED: "Selectat"
|
STR_SELECTED: "Selectat"
|
||||||
STR_TOGGLE: "Schimbă"
|
STR_TOGGLE: "Schimbă"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Comută marcajul"
|
||||||
|
STR_BOOKMARK_REMOVED: "Marcaj eliminat."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Țineți apăsat Deschideți pentru a șterge"
|
||||||
STR_CONFIRM: "Confirmă"
|
STR_CONFIRM: "Confirmă"
|
||||||
STR_CANCEL: "Anulare"
|
STR_CANCEL: "Anulare"
|
||||||
STR_CONNECT: "Conectare"
|
STR_CONNECT: "Conectare"
|
||||||
@@ -261,7 +267,6 @@ STR_CHAPTER_PREFIX: "Capitol: "
|
|||||||
STR_PAGES_SEPARATOR: " pagini | "
|
STR_PAGES_SEPARATOR: " pagini | "
|
||||||
STR_BOOK_PREFIX: "Carte: "
|
STR_BOOK_PREFIX: "Carte: "
|
||||||
STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL"
|
STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL"
|
||||||
STR_PERCENT_STEP_HINT: "Stânga/Dreapta: 1% Sus/Jos: 10%"
|
|
||||||
STR_SYNCING_TIME: "Timp de sincronizare..."
|
STR_SYNCING_TIME: "Timp de sincronizare..."
|
||||||
STR_CALC_HASH: "Calcularea hash-ului documentului..."
|
STR_CALC_HASH: "Calcularea hash-ului documentului..."
|
||||||
STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului"
|
STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului"
|
||||||
@@ -293,8 +298,11 @@ STR_NO_FOOTNOTES: "Nicio notă de subsol"
|
|||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Niciodată"
|
STR_SLEEP_NEVER: "Niciodată"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Stânga/Dreapta: 1 min Sus/Jos: 5 min"
|
STR_STEP_HINT_FRONT: "Butoane frontale:"
|
||||||
|
STR_STEP_HINT_SIDE: "Butoane laterale:"
|
||||||
STR_SCREENSHOT_BUTTON: "Captură ecran"
|
STR_SCREENSHOT_BUTTON: "Captură ecran"
|
||||||
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
|
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
|
||||||
STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare"
|
STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Adaugă rețea ascunsă..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduceți numele rețelei (SSID)"
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропуск главы"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропуск главы"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Изменить ориентацию"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Изменить ориентацию"
|
||||||
|
STR_LONG_PRESS_MENU: "Долгое нажатие меню"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Съешь ещё этих мягких французских булок, да выпей же чаю"
|
||||||
STR_FONT_FAMILY: "Шрифт чтения"
|
STR_FONT_FAMILY: "Шрифт чтения"
|
||||||
STR_FONT_SIZE: "Размер шрифта интерфейса"
|
STR_FONT_SIZE: "Размер шрифта интерфейса"
|
||||||
STR_LINE_SPACING: "Межстрочный интервал"
|
STR_LINE_SPACING: "Межстрочный интервал"
|
||||||
@@ -97,6 +99,7 @@ STR_USERNAME: "Имя пользователя"
|
|||||||
STR_PASSWORD: "Пароль"
|
STR_PASSWORD: "Пароль"
|
||||||
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
||||||
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
||||||
|
STR_SEND_METADATA: "Отправлять метаданные"
|
||||||
STR_AUTHENTICATE: "Авторизация"
|
STR_AUTHENTICATE: "Авторизация"
|
||||||
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
@@ -138,10 +141,13 @@ STR_FORCE_REFRESH: "Обновление экрана"
|
|||||||
STR_PORTRAIT: "Портрет"
|
STR_PORTRAIT: "Портрет"
|
||||||
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||||
STR_INVERTED: "Инверсия"
|
STR_INVERTED: "Инверсия"
|
||||||
|
STR_ORIENTATION_INVERTED: "Портрет 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||||
STR_PREV_NEXT: "Назад/Вперёд"
|
STR_PREV_NEXT: "Назад/Вперёд"
|
||||||
STR_NEXT_PREV: "Вперёд/Назад"
|
STR_NEXT_PREV: "Вперёд/Назад"
|
||||||
STR_DISABLED: "Выключены"
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Закладка"
|
||||||
|
STR_DISABLED: "Выключено"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Маленький"
|
STR_SMALL: "Маленький"
|
||||||
@@ -176,8 +182,7 @@ STR_DOWNLOADING: "Загрузка..."
|
|||||||
STR_DOWNLOAD_FAILED: "Ошибка загрузки"
|
STR_DOWNLOAD_FAILED: "Ошибка загрузки"
|
||||||
STR_ERROR_MSG: "Ошибка:"
|
STR_ERROR_MSG: "Ошибка:"
|
||||||
STR_UNNAMED: "Без имени"
|
STR_UNNAMED: "Без имени"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Удерживайте ОТКРЫТЬ для удаления закладки"
|
STR_HOLD_OPEN_TO_DELETE: "Удерживайте Открыть для удаления"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Удерживайте ВЫБРАТЬ для добавления закладки"
|
|
||||||
STR_NO_SERVER_URL: "URL сервера не настроен"
|
STR_NO_SERVER_URL: "URL сервера не настроен"
|
||||||
STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
|
STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
|
||||||
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
|
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
|
||||||
@@ -195,6 +200,7 @@ STR_HOME: "« Главная"
|
|||||||
STR_SELECT: "Выбрать"
|
STR_SELECT: "Выбрать"
|
||||||
STR_SELECTED: "Выбран"
|
STR_SELECTED: "Выбран"
|
||||||
STR_TOGGLE: "Выбор"
|
STR_TOGGLE: "Выбор"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Переключить закладку"
|
||||||
STR_CONFIRM: "Подтв."
|
STR_CONFIRM: "Подтв."
|
||||||
STR_CANCEL: "Отмена"
|
STR_CANCEL: "Отмена"
|
||||||
STR_CONNECT: "Подкл."
|
STR_CONNECT: "Подкл."
|
||||||
@@ -257,6 +263,7 @@ STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
|
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
|
||||||
STR_BOOKMARKS: "Закладки"
|
STR_BOOKMARKS: "Закладки"
|
||||||
STR_BOOKMARK_ADDED: "Закладка добавлена"
|
STR_BOOKMARK_ADDED: "Закладка добавлена"
|
||||||
|
STR_BOOKMARK_REMOVED: "Закладка удалена"
|
||||||
STR_OPDS_BROWSER: "OPDS браузер"
|
STR_OPDS_BROWSER: "OPDS браузер"
|
||||||
STR_SEARCH: "Поиск"
|
STR_SEARCH: "Поиск"
|
||||||
STR_COVER_CUSTOM: "Обложка + Свой"
|
STR_COVER_CUSTOM: "Обложка + Свой"
|
||||||
@@ -292,7 +299,6 @@ STR_CHAPTER_PREFIX: "Глава: "
|
|||||||
STR_PAGES_SEPARATOR: " стр. | "
|
STR_PAGES_SEPARATOR: " стр. | "
|
||||||
STR_BOOK_PREFIX: "Книга: "
|
STR_BOOK_PREFIX: "Книга: "
|
||||||
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
|
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
|
||||||
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
|
|
||||||
STR_SYNCING_TIME: "Синхронизация времени..."
|
STR_SYNCING_TIME: "Синхронизация времени..."
|
||||||
STR_CALC_HASH: "Расчёт хэша документа..."
|
STR_CALC_HASH: "Расчёт хэша документа..."
|
||||||
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
|
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
|
||||||
@@ -326,7 +332,8 @@ STR_LINK: "[ссылка]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
|
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
||||||
STR_SLEEP_NEVER: "Никогда"
|
STR_SLEEP_NEVER: "Никогда"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Влево/Вправо: 1 мин Вверх/Вниз: 5 мин"
|
STR_STEP_HINT_FRONT: "Передние кнопки:"
|
||||||
|
STR_STEP_HINT_SIDE: "Боковые кнопки:"
|
||||||
STR_ADD_SERVER: "Добавить сервер"
|
STR_ADD_SERVER: "Добавить сервер"
|
||||||
STR_SERVER_NAME: "Имя сервера"
|
STR_SERVER_NAME: "Имя сервера"
|
||||||
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
|
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
|
||||||
@@ -377,3 +384,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ошибка записи прошивки"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
|
||||||
STR_RECOVERY_MODE: "Режим восстановления"
|
STR_RECOVERY_MODE: "Режим восстановления"
|
||||||
STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его"
|
STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Добавить скрытую сеть..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Введите имя сети (SSID)"
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
|
||||||
STR_FONT_FAMILY: "Rodina písiem čítačky"
|
STR_FONT_FAMILY: "Rodina písiem čítačky"
|
||||||
STR_FONT_SIZE: "Veľkosť písma rozhrania"
|
STR_FONT_SIZE: "Veľkosť písma rozhrania"
|
||||||
STR_LINE_SPACING: "Riadkovanie čítačky"
|
STR_LINE_SPACING: "Riadkovanie čítačky"
|
||||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Používateľské meno"
|
|||||||
STR_PASSWORD: "Heslo"
|
STR_PASSWORD: "Heslo"
|
||||||
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
|
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
|
||||||
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
|
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
|
||||||
|
STR_SEND_METADATA: "Odosielať metadáta dokumentu"
|
||||||
STR_AUTHENTICATE: "Overiť"
|
STR_AUTHENTICATE: "Overiť"
|
||||||
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
||||||
@@ -136,7 +138,8 @@ STR_PAGE_TURN: "Otáčanie stránok"
|
|||||||
STR_FORCE_REFRESH: "Obnoviť obrazovku"
|
STR_FORCE_REFRESH: "Obnoviť obrazovku"
|
||||||
STR_PORTRAIT: "Na výšku"
|
STR_PORTRAIT: "Na výšku"
|
||||||
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
|
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
|
||||||
STR_INVERTED: "Obrátený"
|
STR_INVERTED: "Invertovaný"
|
||||||
|
STR_ORIENTATION_INVERTED: "Na výšku 180°"
|
||||||
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
|
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
|
||||||
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
|
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
|
||||||
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
|
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
|
||||||
@@ -175,8 +178,7 @@ STR_DOWNLOADING: "Sťahovanie..."
|
|||||||
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
|
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
|
||||||
STR_ERROR_MSG: "Chyba:"
|
STR_ERROR_MSG: "Chyba:"
|
||||||
STR_UNNAMED: "Nepomenované"
|
STR_UNNAMED: "Nepomenované"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Podrž potvrdiť pre vymazanie"
|
STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Podrž tlačidlo Potvrdiť pre vytvorenie záložky."
|
|
||||||
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
|
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
|
||||||
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
|
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
|
||||||
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
|
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
|
||||||
@@ -194,6 +196,7 @@ STR_HOME: "« Domov"
|
|||||||
STR_SELECT: "Vybrať"
|
STR_SELECT: "Vybrať"
|
||||||
STR_SELECTED: "Vybrané"
|
STR_SELECTED: "Vybrané"
|
||||||
STR_TOGGLE: "Prepnúť"
|
STR_TOGGLE: "Prepnúť"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
|
||||||
STR_CONFIRM: "Potvrdiť"
|
STR_CONFIRM: "Potvrdiť"
|
||||||
STR_CANCEL: "Zrušiť"
|
STR_CANCEL: "Zrušiť"
|
||||||
STR_CONNECT: "Pripojiť"
|
STR_CONNECT: "Pripojiť"
|
||||||
@@ -256,6 +259,7 @@ STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
|
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
|
||||||
STR_BOOKMARKS: "Záložky"
|
STR_BOOKMARKS: "Záložky"
|
||||||
STR_BOOKMARK_ADDED: "Záložka pridaná."
|
STR_BOOKMARK_ADDED: "Záložka pridaná."
|
||||||
|
STR_BOOKMARK_REMOVED: "Záložka odstránená."
|
||||||
STR_OPDS_BROWSER: "Prehliadač OPDS"
|
STR_OPDS_BROWSER: "Prehliadač OPDS"
|
||||||
STR_SEARCH: "Hľadať"
|
STR_SEARCH: "Hľadať"
|
||||||
STR_COVER_CUSTOM: "Obálka + Vlastné"
|
STR_COVER_CUSTOM: "Obálka + Vlastné"
|
||||||
@@ -291,7 +295,6 @@ STR_CHAPTER_PREFIX: "Kapitola:"
|
|||||||
STR_PAGES_SEPARATOR: "strán |"
|
STR_PAGES_SEPARATOR: "strán |"
|
||||||
STR_BOOK_PREFIX: "Kniha:"
|
STR_BOOK_PREFIX: "Kniha:"
|
||||||
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
|
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
|
||||||
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
|
|
||||||
STR_SYNCING_TIME: "Čas synchronizácie..."
|
STR_SYNCING_TIME: "Čas synchronizácie..."
|
||||||
STR_CALC_HASH: "Výpočet hashu dokumentu..."
|
STR_CALC_HASH: "Výpočet hashu dokumentu..."
|
||||||
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
|
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
|
||||||
@@ -325,7 +328,8 @@ STR_LINK: "[link]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikdy"
|
STR_SLEEP_NEVER: "Nikdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
|
STR_STEP_HINT_FRONT: "Predné tlačidlá:"
|
||||||
|
STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
|
||||||
STR_ADD_SERVER: "Pridať server"
|
STR_ADD_SERVER: "Pridať server"
|
||||||
STR_SERVER_NAME: "Názov servera"
|
STR_SERVER_NAME: "Názov servera"
|
||||||
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
|
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
|
||||||
@@ -376,3 +380,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
|
||||||
STR_RECOVERY_MODE: "Režim obnovenia"
|
STR_RECOVERY_MODE: "Režim obnovenia"
|
||||||
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
|
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Pridať skrytú sieť..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Zadajte názov siete (SSID)"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ STR_ORIENTATION: "Orientacija branja"
|
|||||||
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
|
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
|
||||||
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
|
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
|
||||||
STR_FONT_FAMILY: "Pisava bralnika"
|
STR_FONT_FAMILY: "Pisava bralnika"
|
||||||
STR_FONT_SIZE: "Velikost pisave"
|
STR_FONT_SIZE: "Velikost pisave"
|
||||||
STR_LINE_SPACING: "Razmik med vrsticami"
|
STR_LINE_SPACING: "Razmik med vrsticami"
|
||||||
@@ -93,6 +94,7 @@ STR_USERNAME: "Uporabniško ime"
|
|||||||
STR_PASSWORD: "Geslo"
|
STR_PASSWORD: "Geslo"
|
||||||
STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
|
STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
|
||||||
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
|
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
|
||||||
|
STR_SEND_METADATA: "Pošlji metapodatke dokumenta"
|
||||||
STR_AUTHENTICATE: "Avtentikacija"
|
STR_AUTHENTICATE: "Avtentikacija"
|
||||||
STR_KOREADER_USERNAME: "KOReader uporabnik"
|
STR_KOREADER_USERNAME: "KOReader uporabnik"
|
||||||
STR_KOREADER_PASSWORD: "KOReader geslo"
|
STR_KOREADER_PASSWORD: "KOReader geslo"
|
||||||
@@ -131,7 +133,8 @@ STR_SLEEP: "Spanje"
|
|||||||
STR_PAGE_TURN: "Obračanje strani"
|
STR_PAGE_TURN: "Obračanje strani"
|
||||||
STR_PORTRAIT: "Pokončno"
|
STR_PORTRAIT: "Pokončno"
|
||||||
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
|
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
|
||||||
STR_INVERTED: "Obrnjeno"
|
STR_INVERTED: "Invertirano"
|
||||||
|
STR_ORIENTATION_INVERTED: "Pokončno 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
|
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
|
||||||
STR_PREV_NEXT: "Nazaj/Naprej"
|
STR_PREV_NEXT: "Nazaj/Naprej"
|
||||||
STR_NEXT_PREV: "Naprej/Nazaj"
|
STR_NEXT_PREV: "Naprej/Nazaj"
|
||||||
@@ -183,6 +186,9 @@ STR_HOME: "« Domov"
|
|||||||
STR_SELECT: "Izberi"
|
STR_SELECT: "Izberi"
|
||||||
STR_SELECTED: "Izbrano"
|
STR_SELECTED: "Izbrano"
|
||||||
STR_TOGGLE: "Preklopi"
|
STR_TOGGLE: "Preklopi"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Preklopi zaznamek"
|
||||||
|
STR_BOOKMARK_REMOVED: "Zaznamek odstranjen."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Držite Odpri za brisanje"
|
||||||
STR_CONFIRM: "Potrdi"
|
STR_CONFIRM: "Potrdi"
|
||||||
STR_CANCEL: "Prekliči"
|
STR_CANCEL: "Prekliči"
|
||||||
STR_CONNECT: "Poveži"
|
STR_CONNECT: "Poveži"
|
||||||
@@ -258,7 +264,6 @@ STR_CHAPTER_PREFIX: "Poglavje: "
|
|||||||
STR_PAGES_SEPARATOR: " strani | "
|
STR_PAGES_SEPARATOR: " strani | "
|
||||||
STR_BOOK_PREFIX: "Knjiga: "
|
STR_BOOK_PREFIX: "Knjiga: "
|
||||||
STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju"
|
STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju"
|
||||||
STR_PERCENT_STEP_HINT: "Levo/desno: 1% Gor/dol: 10%"
|
|
||||||
STR_SYNCING_TIME: "Sinhronizacija časa..."
|
STR_SYNCING_TIME: "Sinhronizacija časa..."
|
||||||
STR_CALC_HASH: "Izračunavanje podpisa dokumenta..."
|
STR_CALC_HASH: "Izračunavanje podpisa dokumenta..."
|
||||||
STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel"
|
STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel"
|
||||||
@@ -290,8 +295,11 @@ STR_NO_FOOTNOTES: "Na tej strani ni opomb"
|
|||||||
STR_LINK: "[povezava]"
|
STR_LINK: "[povezava]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikoli"
|
STR_SLEEP_NEVER: "Nikoli"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Levo/desno: 1 min Gor/dol: 5 min"
|
STR_STEP_HINT_FRONT: "Sprednji gumbi:"
|
||||||
|
STR_STEP_HINT_SIDE: "Stranski gumbi:"
|
||||||
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
|
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
|
||||||
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
|
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
|
||||||
STR_TILT_PAGE_TURN: "Obračanje s priklonom"
|
STR_TILT_PAGE_TURN: "Obračanje s priklonom"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Dodaj skrito omrežje..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Vnesite ime omrežja (SSID)"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sin capítulos"
|
|||||||
STR_END_OF_BOOK: "Fin del libro"
|
STR_END_OF_BOOK: "Fin del libro"
|
||||||
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
||||||
STR_INDEXING: "Indexando"
|
STR_INDEXING: "Indexando"
|
||||||
|
STR_INDEX_FAILED: "No se pudo indexar: libro no válido"
|
||||||
STR_MEMORY_ERROR: "Error de memoria"
|
STR_MEMORY_ERROR: "Error de memoria"
|
||||||
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
||||||
STR_EMPTY_FILE: "Archivo vacío"
|
STR_EMPTY_FILE: "Archivo vacío"
|
||||||
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
|||||||
STR_NO_NETWORKS: "No hay redes disponibles"
|
STR_NO_NETWORKS: "No hay redes disponibles"
|
||||||
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
|
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
|
||||||
STR_SCANNING: "Buscando..."
|
STR_SCANNING: "Buscando..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "Buscando redes Wi-Fi guardadas..."
|
||||||
STR_CONNECTING: "Conectando..."
|
STR_CONNECTING: "Conectando..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "Conectando a una red Wi-Fi guardada..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostrar"
|
||||||
STR_CONNECTED: "¡Conectado!"
|
STR_CONNECTED: "¡Conectado!"
|
||||||
STR_CONNECTION_FAILED: "Error de conexión"
|
STR_CONNECTION_FAILED: "Error de conexión"
|
||||||
STR_FORGET_NETWORK: "¿Olvidar la red?"
|
STR_FORGET_NETWORK: "¿Olvidar la red?"
|
||||||
@@ -69,6 +73,8 @@ STR_IMAGES: "Imágenes"
|
|||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
||||||
STR_IMAGES_SUPPRESS: "Ocultar"
|
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||||
|
STR_EOB_HOME: "Inicio"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continuar con"
|
||||||
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
||||||
STR_ORIENTATION: "Orientación"
|
STR_ORIENTATION: "Orientación"
|
||||||
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
||||||
@@ -77,6 +83,8 @@ STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambiar orient."
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambiar orient."
|
||||||
|
STR_LONG_PRESS_MENU: "Función de pulsación larga"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Benjamín pidió una bebida de kiwi y fresa. Noé, sin vergüenza, la más exquisita champaña del menú"
|
||||||
STR_FONT_FAMILY: "Tipografía"
|
STR_FONT_FAMILY: "Tipografía"
|
||||||
STR_FONT_SIZE: "Tamaño"
|
STR_FONT_SIZE: "Tamaño"
|
||||||
STR_LINE_SPACING: "Interlineado"
|
STR_LINE_SPACING: "Interlineado"
|
||||||
@@ -96,6 +104,7 @@ STR_USERNAME: "Usuario"
|
|||||||
STR_PASSWORD: "Contraseña"
|
STR_PASSWORD: "Contraseña"
|
||||||
STR_SYNC_SERVER_URL: "URL del servidor de sinc."
|
STR_SYNC_SERVER_URL: "URL del servidor de sinc."
|
||||||
STR_DOCUMENT_MATCHING: "Coincidencia de doc."
|
STR_DOCUMENT_MATCHING: "Coincidencia de doc."
|
||||||
|
STR_SEND_METADATA: "Enviar metadatos doc."
|
||||||
STR_AUTHENTICATE: "Autenticar"
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
STR_KOREADER_USERNAME: "Usuario de KOReader"
|
STR_KOREADER_USERNAME: "Usuario de KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
||||||
@@ -136,9 +145,12 @@ STR_FORCE_REFRESH: "Refrescar pant."
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Horizontal (horario)"
|
STR_LANDSCAPE_CW: "Horizontal (horario)"
|
||||||
STR_INVERTED: "Invertido"
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_ORIENTATION_INVERTED: "Al revés"
|
||||||
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
|
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
|
||||||
STR_PREV_NEXT: "Ant./Sig."
|
STR_PREV_NEXT: "Ant./Sig."
|
||||||
STR_NEXT_PREV: "Sig./Ant."
|
STR_NEXT_PREV: "Sig./Ant."
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Marcador"
|
||||||
STR_DISABLED: "Desactivados"
|
STR_DISABLED: "Desactivados"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
@@ -174,8 +186,7 @@ STR_DOWNLOADING: "Descargando..."
|
|||||||
STR_DOWNLOAD_FAILED: "Fallo de descarga"
|
STR_DOWNLOAD_FAILED: "Fallo de descarga"
|
||||||
STR_ERROR_MSG: "Error:"
|
STR_ERROR_MSG: "Error:"
|
||||||
STR_UNNAMED: "Sin nombre"
|
STR_UNNAMED: "Sin nombre"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Mantenga pulsado Confirmar para borrar"
|
STR_HOLD_OPEN_TO_DELETE: "Mantenga pulsado Abrir para borrar"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Mantenga pulsado Confirmar en el lector para crear un marcador."
|
|
||||||
STR_NO_SERVER_URL: "No se configuró URL de servidor"
|
STR_NO_SERVER_URL: "No se configuró URL de servidor"
|
||||||
STR_FETCH_FEED_FAILED: "Fallo al obtener el feed"
|
STR_FETCH_FEED_FAILED: "Fallo al obtener el feed"
|
||||||
STR_PARSE_FEED_FAILED: "Fallo al procesar el feed"
|
STR_PARSE_FEED_FAILED: "Fallo al procesar el feed"
|
||||||
@@ -193,6 +204,7 @@ STR_HOME: "« Inicio"
|
|||||||
STR_SELECT: "Selecc."
|
STR_SELECT: "Selecc."
|
||||||
STR_SELECTED: "Seleccionado"
|
STR_SELECTED: "Seleccionado"
|
||||||
STR_TOGGLE: "Cambiar"
|
STR_TOGGLE: "Cambiar"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
||||||
STR_CONFIRM: "Confirmar"
|
STR_CONFIRM: "Confirmar"
|
||||||
STR_CANCEL: "Cancelar"
|
STR_CANCEL: "Cancelar"
|
||||||
STR_CONNECT: "Conectar"
|
STR_CONNECT: "Conectar"
|
||||||
@@ -256,6 +268,7 @@ STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
|
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
|
||||||
STR_BOOKMARKS: "Marcadores"
|
STR_BOOKMARKS: "Marcadores"
|
||||||
STR_BOOKMARK_ADDED: "Marcador añadido."
|
STR_BOOKMARK_ADDED: "Marcador añadido."
|
||||||
|
STR_BOOKMARK_REMOVED: "Marcador eliminado."
|
||||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
STR_SEARCH: "Buscar"
|
STR_SEARCH: "Buscar"
|
||||||
STR_COVER_CUSTOM: "Portada + Pers."
|
STR_COVER_CUSTOM: "Portada + Pers."
|
||||||
@@ -291,7 +304,6 @@ STR_CHAPTER_PREFIX: "Cap.: "
|
|||||||
STR_PAGES_SEPARATOR: " pág. | "
|
STR_PAGES_SEPARATOR: " pág. | "
|
||||||
STR_BOOK_PREFIX: "Libro: "
|
STR_BOOK_PREFIX: "Libro: "
|
||||||
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
|
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
|
||||||
STR_PERCENT_STEP_HINT: "Izq./Dcha.: 1% | Subir/Bajar: 10%"
|
|
||||||
STR_SYNCING_TIME: "Tiempo de sincronización..."
|
STR_SYNCING_TIME: "Tiempo de sincronización..."
|
||||||
STR_CALC_HASH: "Calculando hash del documento..."
|
STR_CALC_HASH: "Calculando hash del documento..."
|
||||||
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
||||||
@@ -318,13 +330,15 @@ STR_BOOK_S_STYLE: "Estilo del libro"
|
|||||||
STR_EMBEDDED_STYLE: "Estilo integrado"
|
STR_EMBEDDED_STYLE: "Estilo integrado"
|
||||||
STR_FOCUS_READING: "Lectura enfocada"
|
STR_FOCUS_READING: "Lectura enfocada"
|
||||||
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido desde las notas al pie"
|
||||||
STR_SET_SLEEP_COVER: "Pant. sus."
|
STR_SET_SLEEP_COVER: "Pant. sus."
|
||||||
STR_FOOTNOTES: "Pie de página"
|
STR_FOOTNOTES: "Notas al pie"
|
||||||
STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
|
STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
|
||||||
STR_LINK: "[enlace]"
|
STR_LINK: "[enlace]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
|
||||||
STR_SLEEP_NEVER: "Nunca"
|
STR_SLEEP_NEVER: "Nunca"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Izq./Dcha.: 1 min. Subir/Bajar: 5 min."
|
STR_STEP_HINT_FRONT: "Botones frontales:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botones laterales:"
|
||||||
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
|
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
|
||||||
STR_ADD_SERVER: "Agregar servidor"
|
STR_ADD_SERVER: "Agregar servidor"
|
||||||
STR_SERVER_NAME: "Nombre de servidor"
|
STR_SERVER_NAME: "Nombre de servidor"
|
||||||
@@ -376,3 +390,5 @@ STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!"
|
||||||
STR_RECOVERY_MODE: "Modo de recuperación"
|
STR_RECOVERY_MODE: "Modo de recuperación"
|
||||||
STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo"
|
STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Añadir red oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduce el nombre de la red (SSID)"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Inga kapitel"
|
|||||||
STR_END_OF_BOOK: "Slutet på boken"
|
STR_END_OF_BOOK: "Slutet på boken"
|
||||||
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
||||||
STR_INDEXING: "Indexerar"
|
STR_INDEXING: "Indexerar"
|
||||||
|
STR_INDEX_FAILED: "Misslyckades att indexera - ogiltig bok"
|
||||||
STR_MEMORY_ERROR: "Minnesfel"
|
STR_MEMORY_ERROR: "Minnesfel"
|
||||||
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
||||||
STR_EMPTY_FILE: "Tom fil"
|
STR_EMPTY_FILE: "Tom fil"
|
||||||
@@ -61,7 +62,7 @@ STR_CAT_READER: "Läsare"
|
|||||||
STR_CAT_CONTROLS: "Kontroller"
|
STR_CAT_CONTROLS: "Kontroller"
|
||||||
STR_CAT_SYSTEM: "System"
|
STR_CAT_SYSTEM: "System"
|
||||||
STR_SLEEP_SCREEN: "Viloskärm"
|
STR_SLEEP_SCREEN: "Viloskärm"
|
||||||
STR_SEAMLESS_SLEEP: "Sida som viloskärm"
|
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
||||||
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
||||||
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
||||||
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
||||||
@@ -70,6 +71,8 @@ STR_IMAGES: "Bilder"
|
|||||||
STR_IMAGES_DISPLAY: "Visa"
|
STR_IMAGES_DISPLAY: "Visa"
|
||||||
STR_IMAGES_PLACEHOLDER: "Platshållare"
|
STR_IMAGES_PLACEHOLDER: "Platshållare"
|
||||||
STR_IMAGES_SUPPRESS: "Dölj"
|
STR_IMAGES_SUPPRESS: "Dölj"
|
||||||
|
STR_EOB_HOME: "Hem"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Fortsätt med"
|
||||||
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||||
STR_ORIENTATION: "Läsrikting"
|
STR_ORIENTATION: "Läsrikting"
|
||||||
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
||||||
@@ -78,6 +81,8 @@ STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering"
|
||||||
|
STR_LONG_PRESS_MENU: "Långtrycksmeny"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor"
|
||||||
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
||||||
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
|
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
|
||||||
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
|
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
|
||||||
@@ -97,6 +102,7 @@ STR_USERNAME: "Användarnamn"
|
|||||||
STR_PASSWORD: "Lösenord"
|
STR_PASSWORD: "Lösenord"
|
||||||
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
||||||
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
||||||
|
STR_SEND_METADATA: "Skicka dokumentmetadata"
|
||||||
STR_AUTHENTICATE: "Autentisera "
|
STR_AUTHENTICATE: "Autentisera "
|
||||||
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
||||||
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
||||||
@@ -137,9 +143,12 @@ STR_FORCE_REFRESH: "Uppdatera skärmen"
|
|||||||
STR_PORTRAIT: "Porträtt"
|
STR_PORTRAIT: "Porträtt"
|
||||||
STR_LANDSCAPE_CW: "Landskap medurs"
|
STR_LANDSCAPE_CW: "Landskap medurs"
|
||||||
STR_INVERTED: "Inverterad"
|
STR_INVERTED: "Inverterad"
|
||||||
|
STR_ORIENTATION_INVERTED: "Porträtt 180°"
|
||||||
STR_LANDSCAPE_CCW: "Landskap moturs"
|
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||||
STR_PREV_NEXT: "Förra/Nästa"
|
STR_PREV_NEXT: "Förra/Nästa"
|
||||||
STR_NEXT_PREV: "Nästa/Förra"
|
STR_NEXT_PREV: "Nästa/Förra"
|
||||||
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Bokmärke"
|
||||||
STR_DISABLED: "Inaktiverad"
|
STR_DISABLED: "Inaktiverad"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
@@ -175,8 +184,7 @@ STR_DOWNLOADING: "Laddar ner…"
|
|||||||
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
|
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
|
||||||
STR_ERROR_MSG: "Fel:"
|
STR_ERROR_MSG: "Fel:"
|
||||||
STR_UNNAMED: "Ej namngiven"
|
STR_UNNAMED: "Ej namngiven"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Håll ned Bekräfta för att radera"
|
STR_HOLD_OPEN_TO_DELETE: "Håll ned Öppna för att radera"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Håll Bekräfta i läsaren för att skapa ett bokmärke."
|
|
||||||
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
|
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
|
||||||
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
|
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
|
||||||
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
|
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
|
||||||
@@ -194,6 +202,7 @@ STR_HOME: "« Hem"
|
|||||||
STR_SELECT: "Välj "
|
STR_SELECT: "Välj "
|
||||||
STR_SELECTED: "Vald"
|
STR_SELECTED: "Vald"
|
||||||
STR_TOGGLE: "Växla"
|
STR_TOGGLE: "Växla"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Växla bokmärke"
|
||||||
STR_CONFIRM: "Bekräfta"
|
STR_CONFIRM: "Bekräfta"
|
||||||
STR_CANCEL: "Avbryt"
|
STR_CANCEL: "Avbryt"
|
||||||
STR_CONNECT: "Anslut"
|
STR_CONNECT: "Anslut"
|
||||||
@@ -253,10 +262,10 @@ STR_THEME_LYRA: "Lyra"
|
|||||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
|
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
|
||||||
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
||||||
STR_BOOKMARKS: "Bokmärken"
|
STR_BOOKMARKS: "Bokmärken"
|
||||||
STR_BOOKMARK_ADDED: "Bokmärke tillagt."
|
STR_BOOKMARK_ADDED: "Bokmärke tillagt."
|
||||||
|
STR_BOOKMARK_REMOVED: "Bokmärke borttaget."
|
||||||
STR_OPDS_BROWSER: "OPDS-webbläsare"
|
STR_OPDS_BROWSER: "OPDS-webbläsare"
|
||||||
STR_SEARCH: "Sök"
|
STR_SEARCH: "Sök"
|
||||||
STR_COVER_CUSTOM: "Omslag + Valfri"
|
STR_COVER_CUSTOM: "Omslag + Valfri"
|
||||||
@@ -292,7 +301,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
|
|||||||
STR_PAGES_SEPARATOR: " sidor | "
|
STR_PAGES_SEPARATOR: " sidor | "
|
||||||
STR_BOOK_PREFIX: "Bok:"
|
STR_BOOK_PREFIX: "Bok:"
|
||||||
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
|
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
|
||||||
STR_PERCENT_STEP_HINT: "Vänster/Höger: 1% Upp/Ner 10%"
|
|
||||||
STR_SYNCING_TIME: "Synkroniserar tid…"
|
STR_SYNCING_TIME: "Synkroniserar tid…"
|
||||||
STR_CALC_HASH: "Beräknar dokumenthash"
|
STR_CALC_HASH: "Beräknar dokumenthash"
|
||||||
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
|
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
|
||||||
@@ -319,14 +327,16 @@ STR_BOOK_S_STYLE: "Bokstil"
|
|||||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||||
STR_FOCUS_READING: "Fokusläsning"
|
STR_FOCUS_READING: "Fokusläsning"
|
||||||
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Snabbåtergång från fotnoter"
|
||||||
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
||||||
STR_FOOTNOTES: "Fotnoter"
|
STR_FOOTNOTES: "Fotnoter"
|
||||||
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
||||||
STR_LINK: "[länk]"
|
STR_LINK: "[länk]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Aldrig"
|
STR_SLEEP_NEVER: "Aldrig"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vänster/Höger: 1 min Upp/Ner: 5 min"
|
STR_STEP_HINT_FRONT: "Framknappar:"
|
||||||
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
STR_STEP_HINT_SIDE: "Sidoknappar:"
|
||||||
STR_ADD_SERVER: "Lägg till server"
|
STR_ADD_SERVER: "Lägg till server"
|
||||||
STR_SERVER_NAME: "Servernamn"
|
STR_SERVER_NAME: "Servernamn"
|
||||||
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
|
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
|
||||||
@@ -377,3 +387,5 @@ STR_FIRMWARE_WRITE_FAILED: "Skrivning till firmware misslyckades"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Stäng inte av!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Stäng inte av!"
|
||||||
STR_RECOVERY_MODE: "Återställningsläge"
|
STR_RECOVERY_MODE: "Återställningsläge"
|
||||||
STR_RECOVERY_MODE_HINT: "Placera firmware.bin i SD-kortroten och välj den"
|
STR_RECOVERY_MODE_HINT: "Placera firmware.bin i SD-kortroten och välj den"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Lägg till dolt nätverk..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Ange nätverksnamn (SSID)"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
|
||||||
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
|
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
|
||||||
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
|
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
|
||||||
STR_LINE_SPACING: "Okuyucu Satır Aralığı"
|
STR_LINE_SPACING: "Okuyucu Satır Aralığı"
|
||||||
@@ -91,6 +92,7 @@ STR_USERNAME: "Kullanıcı Adı"
|
|||||||
STR_PASSWORD: "Şifre"
|
STR_PASSWORD: "Şifre"
|
||||||
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
|
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
|
||||||
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
|
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
|
||||||
|
STR_SEND_METADATA: "Belge üst verisi gönder"
|
||||||
STR_AUTHENTICATE: "Kimlik Doğrula"
|
STR_AUTHENTICATE: "Kimlik Doğrula"
|
||||||
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
|
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
|
||||||
STR_KOREADER_PASSWORD: "KOReader Şifresi"
|
STR_KOREADER_PASSWORD: "KOReader Şifresi"
|
||||||
@@ -129,7 +131,8 @@ STR_SLEEP: "Uyku"
|
|||||||
STR_PAGE_TURN: "Sayfa Çevirme"
|
STR_PAGE_TURN: "Sayfa Çevirme"
|
||||||
STR_PORTRAIT: "Dikey"
|
STR_PORTRAIT: "Dikey"
|
||||||
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
|
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
|
||||||
STR_INVERTED: "Ters"
|
STR_INVERTED: "Negatif"
|
||||||
|
STR_ORIENTATION_INVERTED: "Dikey 180°"
|
||||||
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
|
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
|
||||||
STR_PREV_NEXT: "Önceki/Sonraki"
|
STR_PREV_NEXT: "Önceki/Sonraki"
|
||||||
STR_NEXT_PREV: "Sonraki/Önceki"
|
STR_NEXT_PREV: "Sonraki/Önceki"
|
||||||
@@ -180,6 +183,9 @@ STR_EXIT: "« Çıkış"
|
|||||||
STR_HOME: "« Ana Sayfa"
|
STR_HOME: "« Ana Sayfa"
|
||||||
STR_SELECT: "Seç"
|
STR_SELECT: "Seç"
|
||||||
STR_TOGGLE: "Değiştir"
|
STR_TOGGLE: "Değiştir"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Yer imini değiştir"
|
||||||
|
STR_BOOKMARK_REMOVED: "Yer imi kaldırıldı."
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Silmek için Aç düğmesini basılı tutun"
|
||||||
STR_CONFIRM: "Onayla"
|
STR_CONFIRM: "Onayla"
|
||||||
STR_CANCEL: "İptal"
|
STR_CANCEL: "İptal"
|
||||||
STR_CONNECT: "Bağlan"
|
STR_CONNECT: "Bağlan"
|
||||||
@@ -236,7 +242,6 @@ STR_CHAPTER_PREFIX: "Bölüm: "
|
|||||||
STR_PAGES_SEPARATOR: " sayfa | "
|
STR_PAGES_SEPARATOR: " sayfa | "
|
||||||
STR_BOOK_PREFIX: "Kitap: "
|
STR_BOOK_PREFIX: "Kitap: "
|
||||||
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
|
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
|
||||||
STR_PERCENT_STEP_HINT: "Sol/Sağ: %1 Yukarı/Aşağı: %10"
|
|
||||||
STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
|
STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
|
||||||
STR_CALC_HASH: "Belge özeti hesaplanıyor..."
|
STR_CALC_HASH: "Belge özeti hesaplanıyor..."
|
||||||
STR_HASH_FAILED: "Belge özeti hesaplanamadı"
|
STR_HASH_FAILED: "Belge özeti hesaplanamadı"
|
||||||
@@ -284,7 +289,8 @@ STR_IMAGES_SUPPRESS: "Bastır"
|
|||||||
STR_LINK: "[bağlantı]"
|
STR_LINK: "[bağlantı]"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
|
||||||
STR_SLEEP_NEVER: "Asla"
|
STR_SLEEP_NEVER: "Asla"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Sol/Sağ: 1 dak Yukarı/Aşağı: 5 dak"
|
STR_STEP_HINT_FRONT: "Ön tuşlar:"
|
||||||
|
STR_STEP_HINT_SIDE: "Yan tuşlar:"
|
||||||
STR_NO_FILES_FOUND: "Dosya bulunamadı"
|
STR_NO_FILES_FOUND: "Dosya bulunamadı"
|
||||||
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
|
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
|
||||||
STR_PREVIEW: "Önizleme"
|
STR_PREVIEW: "Önizleme"
|
||||||
@@ -298,3 +304,5 @@ STR_SELECTED: "Seçili"
|
|||||||
STR_SHOW: "Göster"
|
STR_SHOW: "Göster"
|
||||||
STR_TITLE: "Başlık"
|
STR_TITLE: "Başlık"
|
||||||
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ STR_CAT_READER: "Читач"
|
|||||||
STR_CAT_CONTROLS: "Кнопки"
|
STR_CAT_CONTROLS: "Кнопки"
|
||||||
STR_CAT_SYSTEM: "Система"
|
STR_CAT_SYSTEM: "Система"
|
||||||
STR_SLEEP_SCREEN: "Екран у режимі сну"
|
STR_SLEEP_SCREEN: "Екран у режимі сну"
|
||||||
|
STR_QUICK_RESUME_TIMEOUT: "Швидке поверн. після таймауту"
|
||||||
STR_SLEEP_COVER_MODE: "Режим заповнення"
|
STR_SLEEP_COVER_MODE: "Режим заповнення"
|
||||||
STR_HIDE_BATTERY: "Приховати % батареї"
|
STR_HIDE_BATTERY: "Приховати % батареї"
|
||||||
STR_EXTRA_SPACING: "Додатковий інтервал між абзацами"
|
STR_EXTRA_SPACING: "Додатковий інтервал між абзацами"
|
||||||
@@ -77,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настик
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Наступ. розділ (утримув.)"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Наступ. розділ (утримув.)"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Зміна орієнтації екрану"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Зміна орієнтації екрану"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Єхидна, ґава, їжак ще й шиплячі плазуни бігцем форсують Янцзи"
|
||||||
STR_FONT_FAMILY: "Шрифт"
|
STR_FONT_FAMILY: "Шрифт"
|
||||||
STR_FONT_SIZE: "Розмір шрифту"
|
STR_FONT_SIZE: "Розмір шрифту"
|
||||||
STR_LINE_SPACING: "Міжрядковий інтервал"
|
STR_LINE_SPACING: "Міжрядковий інтервал"
|
||||||
@@ -85,8 +87,8 @@ STR_PARA_ALIGNMENT: "Вирівнювання тексту"
|
|||||||
STR_HYPHENATION: "Перенесення слів"
|
STR_HYPHENATION: "Перенесення слів"
|
||||||
STR_TIME_TO_SLEEP: "Перехід в режим сну"
|
STR_TIME_TO_SLEEP: "Перехід в режим сну"
|
||||||
STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
|
STR_SHOW_HIDDEN_FILES: "Показати приховані файли"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Очищати прочитані книги зі списку останніх"
|
STR_REMOVE_READ_FROM_RECENTS: "Приховувати прочитані книги"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Переміщати прочитані книги до теки Read"
|
STR_MOVE_FINISHED_TO_READ: "Переміщати прочит. в теку Read"
|
||||||
STR_REFRESH_FREQ: "Частота оновлення екрану"
|
STR_REFRESH_FREQ: "Частота оновлення екрану"
|
||||||
STR_KOREADER_SYNC: "Синхронізація KOReader"
|
STR_KOREADER_SYNC: "Синхронізація KOReader"
|
||||||
STR_CHECK_UPDATES: "Перевірити оновлення системи"
|
STR_CHECK_UPDATES: "Перевірити оновлення системи"
|
||||||
@@ -96,6 +98,7 @@ STR_USERNAME: "Ім'я користувача"
|
|||||||
STR_PASSWORD: "Пароль"
|
STR_PASSWORD: "Пароль"
|
||||||
STR_SYNC_SERVER_URL: "URL для синхронізації"
|
STR_SYNC_SERVER_URL: "URL для синхронізації"
|
||||||
STR_DOCUMENT_MATCHING: "Порівняння документів"
|
STR_DOCUMENT_MATCHING: "Порівняння документів"
|
||||||
|
STR_SEND_METADATA: "Надсилати метадані"
|
||||||
STR_AUTHENTICATE: "Автентифікувати"
|
STR_AUTHENTICATE: "Автентифікувати"
|
||||||
STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
|
STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
@@ -135,10 +138,12 @@ STR_PAGE_TURN: "Наст. сторінка"
|
|||||||
STR_FORCE_REFRESH: "Оновити екран"
|
STR_FORCE_REFRESH: "Оновити екран"
|
||||||
STR_PORTRAIT: "Книжкова"
|
STR_PORTRAIT: "Книжкова"
|
||||||
STR_LANDSCAPE_CW: "Альбом. за год."
|
STR_LANDSCAPE_CW: "Альбом. за год."
|
||||||
STR_INVERTED: "Перевернутий"
|
STR_INVERTED: "Інверсія"
|
||||||
|
STR_ORIENTATION_INVERTED: "Книжкова 180°"
|
||||||
STR_LANDSCAPE_CCW: "Альбом. проти год."
|
STR_LANDSCAPE_CCW: "Альбом. проти год."
|
||||||
STR_PREV_NEXT: "Попер/Наст"
|
STR_PREV_NEXT: "Попер/Наст"
|
||||||
STR_NEXT_PREV: "Наст/Попер"
|
STR_NEXT_PREV: "Наст/Попер"
|
||||||
|
STR_DISABLED: "Вимкнуто"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Малий"
|
STR_SMALL: "Малий"
|
||||||
@@ -173,6 +178,7 @@ STR_DOWNLOADING: "Завантаження..."
|
|||||||
STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
|
STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
|
||||||
STR_ERROR_MSG: "Помилка:"
|
STR_ERROR_MSG: "Помилка:"
|
||||||
STR_UNNAMED: "Без назви"
|
STR_UNNAMED: "Без назви"
|
||||||
|
STR_HOLD_OPEN_TO_DELETE: "Утримуйте Відкрити, щоб видалити"
|
||||||
STR_NO_SERVER_URL: "URL сервера не налаштовано"
|
STR_NO_SERVER_URL: "URL сервера не налаштовано"
|
||||||
STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку"
|
STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку"
|
||||||
STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку"
|
STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку"
|
||||||
@@ -190,6 +196,7 @@ STR_HOME: "« Додому"
|
|||||||
STR_SELECT: "Вибрати"
|
STR_SELECT: "Вибрати"
|
||||||
STR_SELECTED: "Вибрано"
|
STR_SELECTED: "Вибрано"
|
||||||
STR_TOGGLE: "Обрати"
|
STR_TOGGLE: "Обрати"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Перемкнути закладку"
|
||||||
STR_CONFIRM: "Підтвердити"
|
STR_CONFIRM: "Підтвердити"
|
||||||
STR_CANCEL: "Скасувати"
|
STR_CANCEL: "Скасувати"
|
||||||
STR_CONNECT: "Приєдн."
|
STR_CONNECT: "Приєдн."
|
||||||
@@ -228,18 +235,35 @@ STR_BATTERY: "Акумулятор"
|
|||||||
STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
|
STR_XTC_STATUS_BAR: "XTC Рядок прогресу"
|
||||||
STR_BOTTOM: "Низ"
|
STR_BOTTOM: "Низ"
|
||||||
STR_TOP: "Верх"
|
STR_TOP: "Верх"
|
||||||
|
STR_CLOCK: "Годинник"
|
||||||
|
STR_CLOCK_UTC_OFFSET: "Часовий пояс"
|
||||||
|
STR_CLOCK_FORMAT: "Формат Годинника"
|
||||||
|
STR_CLOCK_FORMAT_24H: "24-години"
|
||||||
|
STR_CLOCK_FORMAT_12H: "12-годин"
|
||||||
|
STR_CURRENT_TIME: "Поточний час:"
|
||||||
|
STR_NEXT_FIELD: "наступний"
|
||||||
|
STR_CLOCK_SYNC: "Синхр. Годинник"
|
||||||
|
STR_CLOCK_SYNC_NOW: "Синхр. годин. зараз"
|
||||||
|
STR_CLOCK_SYNCING: "Синхр. через NTP..."
|
||||||
|
STR_CLOCK_SYNC_OK: "Успішна синхр."
|
||||||
|
STR_CLOCK_SYNC_FAIL: "Невдала синхр. "
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi не під'єднано"
|
||||||
|
STR_CLOCK_SYNC_NO_WIFI_HINT: "Під'єднайтесь спершу до Wi-Fi, тоді спробуйте знову."
|
||||||
|
STR_CLOCK_SYNCED: "Годинник синхр."
|
||||||
STR_UI_THEME: "Тема інтерфейсу"
|
STR_UI_THEME: "Тема інтерфейсу"
|
||||||
STR_THEME_CLASSIC: "Класична"
|
STR_THEME_CLASSIC: "Класична"
|
||||||
STR_THEME_LYRA: "Lyra"
|
STR_THEME_LYRA: "Lyra"
|
||||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці"
|
STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Швидке продовження після таймауту"
|
|
||||||
STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки"
|
STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки"
|
||||||
|
STR_BOOKMARKS: "Закладки"
|
||||||
|
STR_BOOKMARK_ADDED: "Закладку додано"
|
||||||
|
STR_BOOKMARK_REMOVED: "Закладку видалено"
|
||||||
STR_OPDS_BROWSER: "Браузер OPDS"
|
STR_OPDS_BROWSER: "Браузер OPDS"
|
||||||
STR_SEARCH: "Пошук"
|
STR_SEARCH: "Пошук"
|
||||||
STR_COVER_CUSTOM: "Обкл. + власне"
|
STR_COVER_CUSTOM: "Обкл. + власне"
|
||||||
STR_QUICK_RESUME: "Швидке продовження"
|
STR_QUICK_RESUME: "Швидке поверн."
|
||||||
STR_MENU_RECENT_BOOKS: "Останні книги"
|
STR_MENU_RECENT_BOOKS: "Останні книги"
|
||||||
STR_REMOVE_FROM_RECENTS: "Видалити з останніх книг?"
|
STR_REMOVE_FROM_RECENTS: "Видалити з останніх книг?"
|
||||||
STR_NO_RECENT_BOOKS: "Немає останніх книг"
|
STR_NO_RECENT_BOOKS: "Немає останніх книг"
|
||||||
@@ -265,12 +289,12 @@ STR_GO_HOME_BUTTON: "На головну"
|
|||||||
STR_SYNC_PROGRESS: "Прогрес синхронізації"
|
STR_SYNC_PROGRESS: "Прогрес синхронізації"
|
||||||
STR_DELETE_CACHE: "Видалити кеш книги"
|
STR_DELETE_CACHE: "Видалити кеш книги"
|
||||||
STR_DELETE: "Видалити"
|
STR_DELETE: "Видалити"
|
||||||
|
STR_CONFIRM_DELETE_BOOKMARK: "Видалити цю закладку?"
|
||||||
STR_DISPLAY_QR: "Показати сторінку як QR-код"
|
STR_DISPLAY_QR: "Показати сторінку як QR-код"
|
||||||
STR_CHAPTER_PREFIX: "Розділ: "
|
STR_CHAPTER_PREFIX: "Розділ: "
|
||||||
STR_PAGES_SEPARATOR: " сторінок | "
|
STR_PAGES_SEPARATOR: " сторінок | "
|
||||||
STR_BOOK_PREFIX: "Книга: "
|
STR_BOOK_PREFIX: "Книга: "
|
||||||
STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL"
|
STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL"
|
||||||
STR_PERCENT_STEP_HINT: "Вліво/Вправо: 1% Вгору/Вниз: 10%"
|
|
||||||
STR_SYNCING_TIME: "Синхронізація часу..."
|
STR_SYNCING_TIME: "Синхронізація часу..."
|
||||||
STR_CALC_HASH: "Обчислення хешу документа..."
|
STR_CALC_HASH: "Обчислення хешу документа..."
|
||||||
STR_HASH_FAILED: "Не вдалося обчислити хеш документа"
|
STR_HASH_FAILED: "Не вдалося обчислити хеш документа"
|
||||||
@@ -297,14 +321,16 @@ STR_BOOK_S_STYLE: "Стиль книги"
|
|||||||
STR_EMBEDDED_STYLE: "Вбудований стиль"
|
STR_EMBEDDED_STYLE: "Вбудований стиль"
|
||||||
STR_FOCUS_READING: "Фокусне читання"
|
STR_FOCUS_READING: "Фокусне читання"
|
||||||
STR_OPDS_SERVER_URL: "URL сервера OPDS"
|
STR_OPDS_SERVER_URL: "URL сервера OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Швидке повернення з приміток"
|
||||||
STR_SET_SLEEP_COVER: "Як обкл."
|
STR_SET_SLEEP_COVER: "Як обкл."
|
||||||
STR_FOOTNOTES: "Примітки"
|
STR_FOOTNOTES: "Примітки"
|
||||||
STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
|
STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
|
||||||
STR_LINK: "[посилання]"
|
STR_LINK: "[посилання]"
|
||||||
|
STR_SCREENSHOT_BUTTON: "Знімок екрана"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
|
||||||
STR_SLEEP_NEVER: "Ніколи"
|
STR_SLEEP_NEVER: "Ніколи"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Вліво/Вправо: 1 хв Вгору/Вниз: 5 хв"
|
STR_STEP_HINT_FRONT: "Передні кнопки:"
|
||||||
STR_SCREENSHOT_BUTTON: "Знімок екрана"
|
STR_STEP_HINT_SIDE: "Бічні кнопки:"
|
||||||
STR_ADD_SERVER: "Додати сервер"
|
STR_ADD_SERVER: "Додати сервер"
|
||||||
STR_SERVER_NAME: "Назва сервера"
|
STR_SERVER_NAME: "Назва сервера"
|
||||||
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
|
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
|
||||||
@@ -355,3 +381,5 @@ STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
|
||||||
STR_RECOVERY_MODE: "Режим відновлення"
|
STR_RECOVERY_MODE: "Режим відновлення"
|
||||||
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
|
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Додати приховану мережу..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Введіть назву мережі (SSID)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
|
|||||||
STR_SLEEPING: "ENTRANT EN REPÒS"
|
STR_SLEEPING: "ENTRANT EN REPÒS"
|
||||||
STR_ENTERING_SLEEP: "Entrant en repòs"
|
STR_ENTERING_SLEEP: "Entrant en repòs"
|
||||||
STR_BROWSE_FILES: "Explora arxius"
|
STR_BROWSE_FILES: "Explora arxius"
|
||||||
STR_FILE_TRANSFER: "Transferència"
|
STR_FILE_TRANSFER: "Transferència d'arxius"
|
||||||
STR_SETTINGS_TITLE: "Configuració"
|
STR_SETTINGS_TITLE: "Configuració"
|
||||||
STR_CONTINUE_READING: "Continua llegint"
|
STR_CONTINUE_READING: "Continua llegint"
|
||||||
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
||||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
|
|||||||
STR_END_OF_BOOK: "Final del llibre"
|
STR_END_OF_BOOK: "Final del llibre"
|
||||||
STR_EMPTY_CHAPTER: "Capítol buit"
|
STR_EMPTY_CHAPTER: "Capítol buit"
|
||||||
STR_INDEXING: "S'està indexant"
|
STR_INDEXING: "S'està indexant"
|
||||||
|
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
|
||||||
STR_MEMORY_ERROR: "Error de memòria"
|
STR_MEMORY_ERROR: "Error de memòria"
|
||||||
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||||
STR_EMPTY_FILE: "Arxiu buit"
|
STR_EMPTY_FILE: "Arxiu buit"
|
||||||
@@ -28,58 +29,65 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
|||||||
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
||||||
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
||||||
STR_SCANNING: "S'està escanejant..."
|
STR_SCANNING: "S'està escanejant..."
|
||||||
|
STR_FINDING_SAVED_WIFI: "S'estan buscant xarxes Wi-Fi guardades..."
|
||||||
STR_CONNECTING: "S'està connectant..."
|
STR_CONNECTING: "S'està connectant..."
|
||||||
|
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi guardada..."
|
||||||
|
STR_SHOW_NETWORKS: "Mostra"
|
||||||
STR_CONNECTED: "S'ha connectat!"
|
STR_CONNECTED: "S'ha connectat!"
|
||||||
STR_CONNECTION_FAILED: "Error de connexió"
|
STR_CONNECTION_FAILED: "Error de connexió"
|
||||||
STR_FORGET_NETWORK: "Voleu oblidar esta xarxa?"
|
STR_FORGET_NETWORK: "Vols oblidar esta xarxa?"
|
||||||
STR_SAVE_PASSWORD: "Voleu guardar la contrasenya per a la pròxima vegada?"
|
STR_SAVE_PASSWORD: "Vols guardar la contrasenya per a la pròxima vegada?"
|
||||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
STR_JOIN_NETWORK: "Unix-te a una xarxa"
|
||||||
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
||||||
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||||
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
||||||
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
||||||
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a esta xarxa Wi-Fi"
|
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a esta xarxa Wi-Fi"
|
||||||
STR_OPEN_URL_HINT: "Obriu este URL al navegador"
|
STR_OPEN_URL_HINT: "Obri este URL al navegador"
|
||||||
STR_OR_HTTP_PREFIX: "o http://"
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
||||||
STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
|
STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
|
||||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||||
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Introduïx la contrasenya Wi-Fi"
|
||||||
STR_TO_PREFIX: "a "
|
STR_TO_PREFIX: "a "
|
||||||
STR_CALIBRE_RECEIVING: "S'està rebent: "
|
STR_CALIBRE_RECEIVING: "S'està rebent: "
|
||||||
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
||||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu esta pantalla oberta mentre s'envia\""
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantín esta pantalla oberta mentre s'envia\""
|
||||||
STR_CAT_DISPLAY: "Visualització"
|
STR_CAT_DISPLAY: "Visualització"
|
||||||
STR_CAT_READER: "Lector"
|
STR_CAT_READER: "Lector"
|
||||||
STR_CAT_CONTROLS: "Controls"
|
STR_CAT_CONTROLS: "Controls"
|
||||||
STR_CAT_SYSTEM: "Sistema"
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||||
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
|
STR_SLEEP_COVER_MODE: "Ajust de la portada en repòs"
|
||||||
STR_HIDE_BATTERY: "Oculta el % de bateria"
|
STR_HIDE_BATTERY: "Oculta el % de bateria"
|
||||||
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
|
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
|
||||||
STR_TEXT_AA: "Antialiàsing del text"
|
STR_TEXT_AA: "Antialiàsing del text"
|
||||||
STR_IMAGES: "Imatges"
|
STR_IMAGES: "Imatges"
|
||||||
STR_IMAGES_DISPLAY: "Mostrar"
|
STR_IMAGES_DISPLAY: "Mostrar"
|
||||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||||
STR_IMAGES_SUPPRESS: "Eliminar"
|
STR_IMAGES_SUPPRESS: "Eliminar"
|
||||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
STR_EOB_HOME: "Inici"
|
||||||
|
STR_EOB_CONTINUE_WITH: "Continua amb"
|
||||||
|
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
|
||||||
STR_ORIENTATION: "Orientació de lectura"
|
STR_ORIENTATION: "Orientació de lectura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
STR_LONG_PRESS_BEHAVIOR: "Acció en mantindre premut un botó"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||||
|
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
||||||
STR_FONT_FAMILY: "Família de fonts"
|
STR_FONT_FAMILY: "Família de fonts"
|
||||||
STR_FONT_SIZE: "Grandària de la lletra (UI)"
|
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||||
STR_LINE_SPACING: "Interlineat del lector"
|
STR_LINE_SPACING: "Interlineat del lector"
|
||||||
STR_SCREEN_MARGIN: "Marge de pantalla del lector"
|
STR_SCREEN_MARGIN: "Marge de pantalla del lector"
|
||||||
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
||||||
@@ -87,25 +95,27 @@ STR_HYPHENATION: "Partició de mots"
|
|||||||
STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
|
STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Mai"
|
STR_SLEEP_NEVER: "Mai"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min"
|
STR_STEP_HINT_FRONT: "Botons frontals:"
|
||||||
|
STR_STEP_HINT_SIDE: "Botons laterals:"
|
||||||
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
|
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
|
||||||
STR_REFRESH_FREQ: "Freqüència de refresc"
|
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||||
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
||||||
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
|
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
|
||||||
STR_LANGUAGE: "Idioma"
|
STR_LANGUAGE: "Llengua"
|
||||||
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
||||||
STR_USERNAME: "Nom d'usuari"
|
STR_USERNAME: "Nom d'usuari"
|
||||||
STR_PASSWORD: "Contrasenya"
|
STR_PASSWORD: "Contrasenya"
|
||||||
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
||||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||||
|
STR_SEND_METADATA: "Envia metadades del document"
|
||||||
STR_AUTHENTICATE: "Autentica"
|
STR_AUTHENTICATE: "Autentica"
|
||||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||||
STR_FILENAME: "Nom d'arxiu"
|
STR_FILENAME: "Nom d'arxiu"
|
||||||
STR_BINARY: "Binari"
|
STR_BINARY: "Binari"
|
||||||
STR_SET_CREDENTIALS_FIRST: "Estableix les credencials primer"
|
STR_SET_CREDENTIALS_FIRST: "Establix les credencials primer"
|
||||||
STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida"
|
STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida"
|
||||||
STR_AUTHENTICATING: "S'està autenticant..."
|
STR_AUTHENTICATING: "S'està autenticant..."
|
||||||
STR_AUTH_SUCCESS: "Autenticació correcta!"
|
STR_AUTH_SUCCESS: "Autenticació correcta!"
|
||||||
@@ -139,10 +149,13 @@ STR_PAGE_TURN: "Canvi de pàgina"
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Horitzontal horari"
|
STR_LANDSCAPE_CW: "Horitzontal horari"
|
||||||
STR_INVERTED: "Invertit"
|
STR_INVERTED: "Invertit"
|
||||||
|
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||||
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
||||||
STR_PREV_NEXT: "Anterior/Següent"
|
STR_PREV_NEXT: "Anterior/Següent"
|
||||||
STR_NEXT_PREV: "Següent/Anterior"
|
STR_NEXT_PREV: "Següent/Anterior"
|
||||||
STR_DISABLED: "Desactivats"
|
STR_KOSYNC: "KOSync"
|
||||||
|
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||||
|
STR_DISABLED: "Sense funció"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_SMALL: "Petita"
|
STR_SMALL: "Petita"
|
||||||
@@ -170,17 +183,16 @@ STR_UPDATING: "S'està actualitzant..."
|
|||||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
||||||
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
||||||
STR_UPDATE_COMPLETE: "Actualització completada"
|
STR_UPDATE_COMPLETE: "Actualització completada"
|
||||||
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar"
|
STR_POWER_ON_HINT: "Prem i mantín premut el botó d'encesa per tornar a engegar"
|
||||||
STR_NO_ENTRIES: "No s'ha trobat cap entrada"
|
STR_NO_ENTRIES: "No s'ha trobat cap entrada"
|
||||||
STR_DOWNLOADING: "S'està baixant..."
|
STR_DOWNLOADING: "S'està baixant..."
|
||||||
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
||||||
STR_ERROR_MSG: "Error:"
|
STR_ERROR_MSG: "Error:"
|
||||||
STR_UNNAMED: "Sense nom"
|
STR_UNNAMED: "Sense nom"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar"
|
STR_HOLD_OPEN_TO_DELETE: "Mantín premut Obri per esborrar"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre."
|
|
||||||
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
||||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||||
STR_NETWORK_PREFIX: "Xarxa: "
|
STR_NETWORK_PREFIX: "Xarxa: "
|
||||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||||
@@ -193,10 +205,11 @@ STR_HOME: "« Inici"
|
|||||||
STR_SELECT: "Selecciona"
|
STR_SELECT: "Selecciona"
|
||||||
STR_SELECTED: "Seleccionat"
|
STR_SELECTED: "Seleccionat"
|
||||||
STR_TOGGLE: "Canvia"
|
STR_TOGGLE: "Canvia"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Afig o elimina el punt de llibre"
|
||||||
STR_CONFIRM: "Confirma"
|
STR_CONFIRM: "Confirma"
|
||||||
STR_CANCEL: "Cancel·la"
|
STR_CANCEL: "Cancel·la"
|
||||||
STR_CONNECT: "Connecta"
|
STR_CONNECT: "Connecta"
|
||||||
STR_OPEN: "Obre"
|
STR_OPEN: "Obri"
|
||||||
STR_DOWNLOAD: "Descarrega"
|
STR_DOWNLOAD: "Descarrega"
|
||||||
STR_RETRY: "Reintenta"
|
STR_RETRY: "Reintenta"
|
||||||
STR_YES: "Sí"
|
STR_YES: "Sí"
|
||||||
@@ -211,7 +224,7 @@ STR_DIR_RIGHT: "Dreta"
|
|||||||
STR_DIR_UP: "Amunt"
|
STR_DIR_UP: "Amunt"
|
||||||
STR_DIR_DOWN: "Avall"
|
STR_DIR_DOWN: "Avall"
|
||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
|
STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
|
||||||
STR_FILTER_CONTRAST: "Contrast"
|
STR_FILTER_CONTRAST: "Contrast"
|
||||||
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||||
@@ -236,24 +249,25 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||||
STR_BOOKMARKS: "Punts de llibre"
|
STR_BOOKMARKS: "Punts de llibre"
|
||||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||||
|
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
|
||||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||||
STR_QUICK_RESUME: "Represa ràpida"
|
STR_QUICK_RESUME: "Represa ràpida"
|
||||||
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||||
STR_REMOVE_FROM_RECENTS: "Voleu eliminar-lo de Llibres recents?"
|
STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?"
|
||||||
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
||||||
STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre"
|
STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre"
|
||||||
STR_FORGET_AND_REMOVE: "Voleu eliminar la contrasenya guardada?"
|
STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i eliminar la contrasenya guardada?"
|
||||||
STR_FORGET_BUTTON: "Oblida"
|
STR_FORGET_BUTTON: "Oblida"
|
||||||
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||||
STR_CALIBRE_SETUP: "Configura"
|
STR_CALIBRE_SETUP: "Configura"
|
||||||
STR_CALIBRE_STATUS: "Estat"
|
STR_CALIBRE_STATUS: "Estat"
|
||||||
STR_CLEAR_BUTTON: "Esborra"
|
STR_CLEAR_BUTTON: "Esborra"
|
||||||
STR_DEFAULT_VALUE: "Per defecte"
|
STR_DEFAULT_VALUE: "Per defecte"
|
||||||
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol"
|
STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
|
||||||
STR_UNASSIGNED: "No assignat"
|
STR_UNASSIGNED: "No assignat"
|
||||||
STR_ALREADY_ASSIGNED: "Ja assignat"
|
STR_ALREADY_ASSIGNED: "Ja assignat"
|
||||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restablix la disposició per defecte"
|
||||||
STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació"
|
STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació"
|
||||||
STR_HW_BACK_LABEL: "Arrere (1r botó)"
|
STR_HW_BACK_LABEL: "Arrere (1r botó)"
|
||||||
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
|
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
|
||||||
@@ -264,20 +278,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
|
|||||||
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
||||||
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
||||||
STR_DELETE: "Esborra"
|
STR_DELETE: "Esborra"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar este punt de llibre?"
|
STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar este punt de llibre?"
|
||||||
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
||||||
STR_CHAPTER_PREFIX: "Capítol: "
|
STR_CHAPTER_PREFIX: "Capítol: "
|
||||||
STR_PAGES_SEPARATOR: " pàgines | "
|
STR_PAGES_SEPARATOR: " pàgines | "
|
||||||
STR_BOOK_PREFIX: "Llibre: "
|
STR_BOOK_PREFIX: "Llibre: "
|
||||||
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
|
STR_CALIBRE_URL_HINT: "Per al Calibre, afig /opds a la URL"
|
||||||
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
|
|
||||||
STR_SYNCING_TIME: "S'està sincronitzant el temps..."
|
STR_SYNCING_TIME: "S'està sincronitzant el temps..."
|
||||||
STR_CALC_HASH: "S'està calculant el hash del document..."
|
STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
|
||||||
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
|
STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
|
||||||
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
||||||
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||||
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
||||||
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader en Configuració"
|
STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader en Configuració"
|
||||||
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
||||||
STR_REMOTE_LABEL: "Remot:"
|
STR_REMOTE_LABEL: "Remot:"
|
||||||
STR_LOCAL_LABEL: "Local:"
|
STR_LOCAL_LABEL: "Local:"
|
||||||
@@ -287,7 +300,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
|||||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||||
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
||||||
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?"
|
STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
|
||||||
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
||||||
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés"
|
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés"
|
||||||
@@ -297,15 +310,16 @@ STR_BOOK_S_STYLE: "Estil del llibre"
|
|||||||
STR_EMBEDDED_STYLE: "Estil incrustat"
|
STR_EMBEDDED_STYLE: "Estil incrustat"
|
||||||
STR_FOCUS_READING: "Lectura enfocada"
|
STR_FOCUS_READING: "Lectura enfocada"
|
||||||
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||||
|
STR_PWR_BTN_FOOTNOTE_BACK: "Retorn ràpid des de les notes al peu"
|
||||||
STR_FOOTNOTES: "Notes al peu"
|
STR_FOOTNOTES: "Notes al peu"
|
||||||
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
|
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
|
||||||
STR_LINK: "[enllaç]"
|
STR_LINK: "[enllaç]"
|
||||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
|
||||||
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
||||||
STR_FORCE_REFRESH: "Refresca la pantalla"
|
STR_FORCE_REFRESH: "Refresca la pantalla"
|
||||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons."
|
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantín premut el botó d'encesa durant uns segons."
|
||||||
STR_NEXT_PAGE: "Pàgina següent »"
|
STR_NEXT_PAGE: "Pàgina següent »"
|
||||||
STR_PREV_PAGE: "« Pàgina anterior"
|
STR_PREV_PAGE: "« Pàgina anterior"
|
||||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||||
@@ -344,35 +358,37 @@ STR_INSTALLED: "Instal·lat"
|
|||||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||||
STR_CRASH_TITLE: "Bloqueig del sistema"
|
STR_CRASH_TITLE: "Fallada del sistema"
|
||||||
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Incloeu este arxiu en l'informe d'errors."
|
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Inclou este arxiu en l'informe d'errors."
|
||||||
STR_CRASH_REASON: "Motiu del bloqueig:"
|
STR_CRASH_REASON: "Motiu de la fallada"
|
||||||
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
||||||
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
||||||
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
||||||
STR_KB_HINT_HIDE_PASSWORD: "Mantén premut Dreta i prem [***] per ocultar la contrasenya"
|
STR_KB_HINT_HIDE_PASSWORD: "Mantín premut Dreta i prem [***] per ocultar la contrasenya"
|
||||||
STR_KB_HINT_SHOW_PASSWORD: "Mantén premut Dreta i prem [abc] per mostrar la contrasenya"
|
STR_KB_HINT_SHOW_PASSWORD: "Mantín premut Dreta i prem [abc] per mostrar la contrasenya"
|
||||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya"
|
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya"
|
||||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya"
|
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya"
|
||||||
STR_KB_HINT_EDIT_ENTRY: "Mantén premut Amunt per editar l'entrada"
|
STR_KB_HINT_EDIT_ENTRY: "Mantín premut Amunt per editar l'entrada"
|
||||||
STR_KB_TIPS: "Consells:"
|
STR_KB_TIPS: "Consells:"
|
||||||
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
||||||
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL"
|
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL"
|
||||||
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
STR_KB_HINT_CLEAR_TEXT: "Mantín premut DEL per esborrar tot el text"
|
||||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
STR_KB_HINT_SECONDARY_CHAR: "Mantín premut SELECT: caràcter secundari"
|
||||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari"
|
STR_KB_HINT_UPPER_SECONDARY: "Mantín premut SELECT: majúscules o caràcter secundari"
|
||||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari"
|
STR_KB_HINT_LOWER_SECONDARY: "Mantín premut SELECT: minúscules o caràcter secundari"
|
||||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||||
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
||||||
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un arxiu de firmware (.bin)"
|
STR_SELECT_FIRMWARE_FILE: "Selecciona un arxiu de firmware (.bin)"
|
||||||
STR_NO_BIN_FILES: "No s'han trobat arxius .bin"
|
STR_NO_BIN_FILES: "No s'han trobat arxius .bin"
|
||||||
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||||
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
||||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||||
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
||||||
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?"
|
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
||||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagues el dispositiu!"
|
||||||
STR_RECOVERY_MODE: "Mode de recuperació"
|
STR_RECOVERY_MODE: "Mode de recuperació"
|
||||||
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo"
|
STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Afig una xarxa oculta..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Introduïx el nom de la xarxa (SSID)"
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ nút"
|
|||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng"
|
||||||
|
STR_FONT_PREVIEW_TEXT: "Trường quê em do bố của em xây kĩ nên sạch và đẹp lắm"
|
||||||
STR_FONT_FAMILY: "Phông chữ trình đọc"
|
STR_FONT_FAMILY: "Phông chữ trình đọc"
|
||||||
STR_FONT_SIZE: "Cỡ chữ trình đọc"
|
STR_FONT_SIZE: "Cỡ chữ trình đọc"
|
||||||
STR_LINE_SPACING: "Giãn dòng trình đọc"
|
STR_LINE_SPACING: "Giãn dòng trình đọc"
|
||||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Tên đăng nhập"
|
|||||||
STR_PASSWORD: "Mật khẩu"
|
STR_PASSWORD: "Mật khẩu"
|
||||||
STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
|
STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
|
||||||
STR_DOCUMENT_MATCHING: "Khớp tài liệu"
|
STR_DOCUMENT_MATCHING: "Khớp tài liệu"
|
||||||
|
STR_SEND_METADATA: "Gửi siêu dữ liệu tài liệu"
|
||||||
STR_AUTHENTICATE: "Xác thực"
|
STR_AUTHENTICATE: "Xác thực"
|
||||||
STR_KOREADER_USERNAME: "Tên đăng nhập KOReader"
|
STR_KOREADER_USERNAME: "Tên đăng nhập KOReader"
|
||||||
STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
|
STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
|
||||||
@@ -136,7 +138,8 @@ STR_PAGE_TURN: "Lật trang"
|
|||||||
STR_FORCE_REFRESH: "Làm tươi màn hình"
|
STR_FORCE_REFRESH: "Làm tươi màn hình"
|
||||||
STR_PORTRAIT: "Dọc"
|
STR_PORTRAIT: "Dọc"
|
||||||
STR_LANDSCAPE_CW: "Ngang (thuận)"
|
STR_LANDSCAPE_CW: "Ngang (thuận)"
|
||||||
STR_INVERTED: "Lật ngược"
|
STR_INVERTED: "Đảo màu"
|
||||||
|
STR_ORIENTATION_INVERTED: "Dọc 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ngang (ngược)"
|
STR_LANDSCAPE_CCW: "Ngang (ngược)"
|
||||||
STR_PREV_NEXT: "Trước/Sau"
|
STR_PREV_NEXT: "Trước/Sau"
|
||||||
STR_NEXT_PREV: "Sau/Trước"
|
STR_NEXT_PREV: "Sau/Trước"
|
||||||
@@ -175,8 +178,7 @@ STR_DOWNLOADING: "Đang tải về..."
|
|||||||
STR_DOWNLOAD_FAILED: "Tải về thất bại"
|
STR_DOWNLOAD_FAILED: "Tải về thất bại"
|
||||||
STR_ERROR_MSG: "Lỗi:"
|
STR_ERROR_MSG: "Lỗi:"
|
||||||
STR_UNNAMED: "Không tên"
|
STR_UNNAMED: "Không tên"
|
||||||
STR_HOLD_CONFIRM_TO_DELETE: "Giữ Xác nhận để xóa"
|
STR_HOLD_OPEN_TO_DELETE: "Giữ Mở để xóa"
|
||||||
STR_BOOKMARK_INSTRUCTIONS: "Giữ Xác nhận trong trình đọc để tạo dấu trang."
|
|
||||||
STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ"
|
STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ"
|
||||||
STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp"
|
STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp"
|
||||||
STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp"
|
STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp"
|
||||||
@@ -194,6 +196,7 @@ STR_HOME: "« Thư viện"
|
|||||||
STR_SELECT: "Chọn"
|
STR_SELECT: "Chọn"
|
||||||
STR_SELECTED: "Đã chọn"
|
STR_SELECTED: "Đã chọn"
|
||||||
STR_TOGGLE: "Bật/Tắt"
|
STR_TOGGLE: "Bật/Tắt"
|
||||||
|
STR_TOGGLE_BOOKMARK: "Bật/tắt dấu trang"
|
||||||
STR_CONFIRM: "Xác nhận"
|
STR_CONFIRM: "Xác nhận"
|
||||||
STR_CANCEL: "Hủy"
|
STR_CANCEL: "Hủy"
|
||||||
STR_CONNECT: "Kết nối"
|
STR_CONNECT: "Kết nối"
|
||||||
@@ -256,6 +259,7 @@ STR_SUNLIGHT_FADING_FIX: "Khắc phục mờ dưới nắng"
|
|||||||
STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước"
|
STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước"
|
||||||
STR_BOOKMARKS: "Dấu trang"
|
STR_BOOKMARKS: "Dấu trang"
|
||||||
STR_BOOKMARK_ADDED: "Đã thêm dấu trang."
|
STR_BOOKMARK_ADDED: "Đã thêm dấu trang."
|
||||||
|
STR_BOOKMARK_REMOVED: "Đã xóa dấu trang."
|
||||||
STR_OPDS_BROWSER: "Trình duyệt OPDS"
|
STR_OPDS_BROWSER: "Trình duyệt OPDS"
|
||||||
STR_SEARCH: "Tìm kiếm"
|
STR_SEARCH: "Tìm kiếm"
|
||||||
STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh"
|
STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh"
|
||||||
@@ -291,7 +295,6 @@ STR_CHAPTER_PREFIX: "Chương: "
|
|||||||
STR_PAGES_SEPARATOR: " trang | "
|
STR_PAGES_SEPARATOR: " trang | "
|
||||||
STR_BOOK_PREFIX: "Sách: "
|
STR_BOOK_PREFIX: "Sách: "
|
||||||
STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL"
|
STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL"
|
||||||
STR_PERCENT_STEP_HINT: "Trái/Phải: 1% Lên/Xuống: 10%"
|
|
||||||
STR_SYNCING_TIME: "Đang đồng bộ giờ..."
|
STR_SYNCING_TIME: "Đang đồng bộ giờ..."
|
||||||
STR_CALC_HASH: "Đang tính mã băm tài liệu..."
|
STR_CALC_HASH: "Đang tính mã băm tài liệu..."
|
||||||
STR_HASH_FAILED: "Không tính được mã băm tài liệu"
|
STR_HASH_FAILED: "Không tính được mã băm tài liệu"
|
||||||
@@ -325,7 +328,8 @@ STR_LINK: "[liên kết]"
|
|||||||
STR_SCREENSHOT_BUTTON: "Chụp màn hình"
|
STR_SCREENSHOT_BUTTON: "Chụp màn hình"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút"
|
||||||
STR_SLEEP_NEVER: "Không bao giờ"
|
STR_SLEEP_NEVER: "Không bao giờ"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Trái/Phải: 1 phút Lên/Xuống: 5 phút"
|
STR_STEP_HINT_FRONT: "Nút trước:"
|
||||||
|
STR_STEP_HINT_SIDE: "Nút bên:"
|
||||||
STR_ADD_SERVER: "Thêm máy chủ"
|
STR_ADD_SERVER: "Thêm máy chủ"
|
||||||
STR_SERVER_NAME: "Tên máy chủ"
|
STR_SERVER_NAME: "Tên máy chủ"
|
||||||
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
|
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
|
||||||
@@ -376,3 +380,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ghi firmware thất bại"
|
|||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!"
|
||||||
STR_RECOVERY_MODE: "Chế độ phục hồi"
|
STR_RECOVERY_MODE: "Chế độ phục hồi"
|
||||||
STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn"
|
STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn"
|
||||||
|
STR_ADD_HIDDEN_NETWORK: "Thêm mạng ẩn..."
|
||||||
|
STR_ENTER_WIFI_SSID: "Nhập tên mạng (SSID)"
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ enum class InflateStatus {
|
|||||||
|
|
||||||
// Streaming deflate decompressor wrapping uzlib.
|
// Streaming deflate decompressor wrapping uzlib.
|
||||||
//
|
//
|
||||||
|
// NOTE: retained ONLY for FontDecompressor's tiny one-shot flash-resident group
|
||||||
|
// decompressions, where uzlib's ~1KB state beats tinfl's ~11KB on the
|
||||||
|
// OOM-sensitive render path. All throughput paths (zip entries, PNG IDAT) use
|
||||||
|
// InflateStream (lib/miniz), which decodes several times faster.
|
||||||
|
//
|
||||||
// Two modes:
|
// Two modes:
|
||||||
// init(false) — one-shot: input is a contiguous buffer, call read() once.
|
// init(false) — one-shot: input is a contiguous buffer, call read() once.
|
||||||
// init(true) — streaming: allocates a 32KB ring buffer for back-references
|
// init(true) — streaming: allocates a 32KB ring buffer for back-references
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ namespace {
|
|||||||
constexpr int MAX_MCU_HEIGHT = 16;
|
constexpr int MAX_MCU_HEIGHT = 16;
|
||||||
constexpr size_t JPEG_DECODER_SIZE = 20 * 1024;
|
constexpr size_t JPEG_DECODER_SIZE = 20 * 1024;
|
||||||
constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024;
|
constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024;
|
||||||
|
constexpr uint32_t FP_ONE = 1UL << 16;
|
||||||
|
|
||||||
// Static file pointer for JPEGDEC open callback.
|
// Static file pointer for JPEGDEC open callback.
|
||||||
// Safe in single-threaded embedded context; never accessed concurrently.
|
// Safe in single-threaded embedded context; never accessed concurrently.
|
||||||
@@ -208,6 +209,9 @@ struct BmpConvertCtx {
|
|||||||
bool needsScaling;
|
bool needsScaling;
|
||||||
uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point
|
uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point
|
||||||
uint32_t scaleY_fp;
|
uint32_t scaleY_fp;
|
||||||
|
bool smoothUpscale;
|
||||||
|
uint32_t smoothScaleX_fp;
|
||||||
|
uint32_t smoothScaleY_fp;
|
||||||
|
|
||||||
// Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels)
|
// Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels)
|
||||||
// Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row
|
// Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row
|
||||||
@@ -219,6 +223,13 @@ struct BmpConvertCtx {
|
|||||||
std::unique_ptr<uint32_t[]> rowAccum;
|
std::unique_ptr<uint32_t[]> rowAccum;
|
||||||
std::unique_ptr<uint32_t[]> rowCount;
|
std::unique_ptr<uint32_t[]> rowCount;
|
||||||
|
|
||||||
|
int smoothNextOutY;
|
||||||
|
int smoothPrevY;
|
||||||
|
std::unique_ptr<uint8_t[]> smoothRows;
|
||||||
|
uint8_t* smoothPrevRow;
|
||||||
|
uint8_t* smoothCurrRow;
|
||||||
|
uint8_t* smoothOutRow;
|
||||||
|
|
||||||
std::unique_ptr<uint8_t[]> bmpRow;
|
std::unique_ptr<uint8_t[]> bmpRow;
|
||||||
|
|
||||||
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer;
|
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer;
|
||||||
@@ -265,6 +276,88 @@ static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY)
|
|||||||
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
|
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches the progressive-JPEG smoothing used by JpegToFramebufferConverter, but stays
|
||||||
|
// local because cover generation streams dithered BMP rows instead of framebuffer pixels.
|
||||||
|
static uint32_t interpolationStep(const int srcSize, const int outSize) {
|
||||||
|
if (srcSize <= 1 || outSize <= 1) return 0;
|
||||||
|
return (static_cast<uint32_t>(srcSize - 1) << 16) / static_cast<uint32_t>(outSize - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t interpolatedSourceFp(const int outIndex, const int outSize, const int srcSize, const uint32_t step) {
|
||||||
|
if (srcSize <= 1 || outSize <= 1) return 0;
|
||||||
|
if (outIndex >= outSize - 1) return static_cast<uint32_t>(srcSize - 1) << 16;
|
||||||
|
return static_cast<uint32_t>(outIndex) * step;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void scaleRowLinear(BmpConvertCtx* ctx, const uint8_t* srcRow, uint8_t* dstRow) {
|
||||||
|
for (int outX = 0; outX < ctx->outWidth; outX++) {
|
||||||
|
const uint32_t srcX_fp = interpolatedSourceFp(outX, ctx->outWidth, ctx->srcWidth, ctx->smoothScaleX_fp);
|
||||||
|
const int x0 = srcX_fp >> 16;
|
||||||
|
const int x1 = (x0 + 1 < ctx->srcWidth) ? (x0 + 1) : x0;
|
||||||
|
const uint32_t fx = srcX_fp & (FP_ONE - 1);
|
||||||
|
dstRow[outX] = static_cast<uint8_t>((srcRow[x0] * (FP_ONE - fx) + srcRow[x1] * fx) >> 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBlendedRow(BmpConvertCtx* ctx, const uint8_t* row0, const uint8_t* row1, const uint32_t fy,
|
||||||
|
const int outY) {
|
||||||
|
const uint32_t invFy = FP_ONE - fy;
|
||||||
|
for (int outX = 0; outX < ctx->outWidth; outX++) {
|
||||||
|
ctx->smoothOutRow[outX] = static_cast<uint8_t>((row0[outX] * invFy + row1[outX] * fy) >> 16);
|
||||||
|
}
|
||||||
|
writeOutputRow(ctx, ctx->smoothOutRow, outY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void processSmoothSourceRow(BmpConvertCtx* ctx, const uint8_t* srcRow, const int srcY) {
|
||||||
|
scaleRowLinear(ctx, srcRow, ctx->smoothCurrRow);
|
||||||
|
|
||||||
|
if (ctx->smoothPrevY < 0) {
|
||||||
|
uint8_t* tmp = ctx->smoothPrevRow;
|
||||||
|
ctx->smoothPrevRow = ctx->smoothCurrRow;
|
||||||
|
ctx->smoothCurrRow = tmp;
|
||||||
|
ctx->smoothPrevY = srcY;
|
||||||
|
if (ctx->srcHeight <= 1) {
|
||||||
|
while (ctx->smoothNextOutY < ctx->outHeight) {
|
||||||
|
writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY);
|
||||||
|
ctx->smoothNextOutY++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ctx->smoothNextOutY < ctx->outHeight) {
|
||||||
|
const uint32_t srcY_fp =
|
||||||
|
interpolatedSourceFp(ctx->smoothNextOutY, ctx->outHeight, ctx->srcHeight, ctx->smoothScaleY_fp);
|
||||||
|
const int y0 = srcY_fp >> 16;
|
||||||
|
const int y1 = (y0 + 1 < ctx->srcHeight) ? (y0 + 1) : y0;
|
||||||
|
if (y1 > srcY) break;
|
||||||
|
|
||||||
|
const uint8_t* row0 = (y0 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow;
|
||||||
|
const uint8_t* row1 = (y1 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow;
|
||||||
|
writeBlendedRow(ctx, row0, row1, srcY_fp & (FP_ONE - 1), ctx->smoothNextOutY);
|
||||||
|
ctx->smoothNextOutY++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* tmp = ctx->smoothPrevRow;
|
||||||
|
ctx->smoothPrevRow = ctx->smoothCurrRow;
|
||||||
|
ctx->smoothCurrRow = tmp;
|
||||||
|
ctx->smoothPrevY = srcY;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finishSmoothUpscale(BmpConvertCtx* ctx) {
|
||||||
|
if (ctx->smoothPrevY < 0) {
|
||||||
|
LOG_ERR("JPG", "No progressive rows decoded for smoothing");
|
||||||
|
ctx->error = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ctx->smoothNextOutY < ctx->outHeight) {
|
||||||
|
writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY);
|
||||||
|
ctx->smoothNextOutY++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Flush one scaled output row from Y-axis accumulators and advance currentOutY
|
// Flush one scaled output row from Y-axis accumulators and advance currentOutY
|
||||||
static void flushScaledRow(BmpConvertCtx* ctx) {
|
static void flushScaledRow(BmpConvertCtx* ctx) {
|
||||||
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
|
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
|
||||||
@@ -344,7 +437,9 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
|
|||||||
for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) {
|
for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) {
|
||||||
const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth;
|
const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth;
|
||||||
|
|
||||||
if (!ctx->needsScaling) {
|
if (ctx->smoothUpscale) {
|
||||||
|
processSmoothSourceRow(ctx, srcRow, y);
|
||||||
|
} else if (!ctx->needsScaling) {
|
||||||
// 1:1 — outWidth == srcWidth, write directly
|
// 1:1 — outWidth == srcWidth, write directly
|
||||||
writeOutputRow(ctx, srcRow, y);
|
writeOutputRow(ctx, srcRow, y);
|
||||||
} else {
|
} else {
|
||||||
@@ -472,6 +567,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool smoothUpscale =
|
||||||
|
progressiveDecode && needsScaling && scaleSrcWidth <= outWidth && scaleSrcHeight <= outHeight;
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
int bytesPerRow;
|
int bytesPerRow;
|
||||||
if (USE_8BIT_OUTPUT && !oneBit) {
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
@@ -496,6 +594,11 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
ctx.needsScaling = needsScaling;
|
ctx.needsScaling = needsScaling;
|
||||||
ctx.scaleX_fp = scaleX_fp;
|
ctx.scaleX_fp = scaleX_fp;
|
||||||
ctx.scaleY_fp = scaleY_fp;
|
ctx.scaleY_fp = scaleY_fp;
|
||||||
|
ctx.smoothUpscale = smoothUpscale;
|
||||||
|
ctx.smoothScaleX_fp = interpolationStep(ctx.srcWidth, outWidth);
|
||||||
|
ctx.smoothScaleY_fp = interpolationStep(ctx.srcHeight, outHeight);
|
||||||
|
ctx.smoothNextOutY = 0;
|
||||||
|
ctx.smoothPrevY = -1;
|
||||||
ctx.error = false;
|
ctx.error = false;
|
||||||
|
|
||||||
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale
|
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale
|
||||||
@@ -512,7 +615,20 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsScaling) {
|
if (smoothUpscale) {
|
||||||
|
// One contiguous allocation avoids three heap blocks while keeping smoothing line-buffered.
|
||||||
|
const size_t smoothRowsBytes = static_cast<size_t>(outWidth) * 3;
|
||||||
|
ctx.smoothRows = makeUniqueNoThrow<uint8_t[]>(smoothRowsBytes);
|
||||||
|
if (!ctx.smoothRows) {
|
||||||
|
LOG_ERR("JPG", "OOM: progressive smoothing buffers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.smoothPrevRow = ctx.smoothRows.get();
|
||||||
|
ctx.smoothCurrRow = ctx.smoothPrevRow + outWidth;
|
||||||
|
ctx.smoothOutRow = ctx.smoothCurrRow + outWidth;
|
||||||
|
LOG_DBG("JPG", "Progressive smoothing: %dx%d -> %dx%d, buffers=%u bytes", ctx.srcWidth, ctx.srcHeight, outWidth,
|
||||||
|
outHeight, static_cast<unsigned>(smoothRowsBytes));
|
||||||
|
} else if (needsScaling) {
|
||||||
ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth);
|
ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth);
|
||||||
ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth);
|
ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth);
|
||||||
if (!ctx.rowAccum || !ctx.rowCount) {
|
if (!ctx.rowAccum || !ctx.rowCount) {
|
||||||
@@ -549,6 +665,10 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
|
|
||||||
rc = jpeg->decode(0, 0, 0);
|
rc = jpeg->decode(0, 0, 0);
|
||||||
|
|
||||||
|
if (rc == 1 && ctx.smoothUpscale && !ctx.error) {
|
||||||
|
finishSmoothUpscale(&ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if (rc != 1 || ctx.error) {
|
if (rc != 1 || ctx.error) {
|
||||||
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user