Compare commits
88 Commits
release/1.
...
a9f5149444
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9f5149444
|
||
|
|
0222cbf19b
|
||
|
|
02f2474e3b
|
||
|
|
f06e3a0a82
|
||
|
|
a585f219f4
|
||
|
|
df6cc637ec
|
||
|
|
4cfe155488
|
||
|
|
f1966f1e26
|
||
|
|
ebcd3a8b94
|
||
|
|
ed8a0feac1
|
||
|
|
12cc7de49e
|
||
|
|
f622e87c10
|
||
|
|
24c1df0308
|
||
|
|
6cc68e828a
|
||
|
|
6097ee03df
|
||
|
|
d11ad45e59
|
||
|
|
b965ce9fb7
|
||
|
|
744d6160e8
|
||
|
|
66f703df69
|
||
|
|
19004eefaa
|
||
|
|
f90aebc891
|
||
|
|
3096d6066b
|
||
|
|
46c2109f1f | ||
|
|
1383d75c84
|
||
|
|
632b76c9ed
|
||
|
|
5dc9d21bdb
|
||
|
|
c1dfe92ea3
|
||
|
|
5816ab2a47 | ||
|
|
2c0a105550 | ||
|
|
82bfbd8fa6
|
||
|
|
6aa0b865c2
|
||
|
|
0c71e0b13f
|
||
|
|
ea11d2f7d3
|
||
|
|
6e51afb977 | ||
|
|
cb24947477 | ||
|
|
31878a77bc
|
||
|
|
21a75c624d
|
||
|
|
8d4bbf284d
|
||
|
|
7a385d78a4 | ||
|
|
905f694576
|
||
|
|
e798065a5c
|
||
|
|
5e269f912f
|
||
|
|
182c236050
|
||
|
|
73cd05827a | ||
|
|
ea32ba0f8d | ||
|
|
f7b1113819 | ||
|
|
228a1cb511 | ||
|
|
0991782fb4 | ||
|
|
3ae1007cbe | ||
|
|
efb9b72e64 | ||
|
|
4a210823a8 | ||
|
|
b72283d304 | ||
|
|
f5b85f5ca1 | ||
|
|
8cf226613b | ||
|
|
7e93411f46 | ||
|
|
44452a42e9 | ||
|
|
0c2df24f5c | ||
|
|
3a12ca2725 | ||
|
|
d4f25c44bf | ||
|
|
98e6789626 | ||
|
|
b5d28a3a9c | ||
|
|
bc12556da1 | ||
|
|
4e7bb8979c | ||
|
|
4edb14bdd9
|
||
|
|
eb79b98f2b | ||
|
|
14ef625679 | ||
|
|
64d161e88b | ||
|
|
a85d5e627b
|
||
|
|
e73bb3213f | ||
|
|
6202bfd651 | ||
|
|
9b04c2ec76 | ||
|
|
ffddc2472b | ||
|
|
5765bbe821 | ||
|
|
b4b028be3a | ||
|
|
f34d7d2aac | ||
|
|
71769490fb | ||
|
|
cda0a3f898 | ||
|
|
7f40c3f477 | ||
|
|
a87eacc6ab | ||
|
|
1caad578fc | ||
|
|
5b90b68e99 | ||
|
|
67ddd60fce | ||
|
|
76908d38e1 | ||
|
|
e6f5fa43e6 | ||
|
|
e7e31ac487 | ||
|
|
9f78fd33e8 | ||
|
|
bd8132a260 | ||
|
|
f89ce514c8 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,9 +3,15 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nStrings.cpp
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
|
||||
# mod
|
||||
mod/*
|
||||
.cursor/*
|
||||
chat-summaries/*
|
||||
@@ -51,7 +51,7 @@ For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) do
|
||||
|
||||
### Web (latest firmware)
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C
|
||||
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
|
||||
6
SCOPE.md
6
SCOPE.md
@@ -25,6 +25,8 @@ usability over "swiss-army-knife" functionality.
|
||||
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
||||
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
||||
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
||||
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
||||
without breaking focus.
|
||||
|
||||
### Out-of-Scope
|
||||
|
||||
@@ -34,8 +36,8 @@ usability over "swiss-army-knife" functionality.
|
||||
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
||||
and complicate the single-core CPU's execution.
|
||||
* **Media Playback:** No Audio players or Audio-books.
|
||||
* **Complex Reader Features:** No highlighting, notes, or dictionary lookup. These features are better suited for
|
||||
devices with better input capabilities and more powerful chips.
|
||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||
capabilities and more powerful chips.
|
||||
|
||||
## 3. Idea Evaluation
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ Accessible by pressing **Confirm** while inside a book.
|
||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||
|
||||
* **Images:** Embedded images in e-books will not render.
|
||||
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
|
||||
|
||||
---
|
||||
|
||||
@@ -242,3 +243,5 @@ pio device monitor
|
||||
```
|
||||
|
||||
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
||||
|
||||
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Check if clang-format is availible
|
||||
command -v clang-format >/dev/null 2>&1 || {
|
||||
printf "'clang-format' not found in current environment\n"
|
||||
printf "install 'clang', 'clang-tools', or 'clang-format' depending on your distro/os and tooling requirements\n"
|
||||
exit 1
|
||||
}
|
||||
|
||||
GIT_LS_FILES_FLAGS=""
|
||||
if [[ "$1" == "-g" ]]; then
|
||||
GIT_LS_FILES_FLAGS="--modified"
|
||||
fi
|
||||
|
||||
|
||||
# --- Main Logic ---
|
||||
|
||||
# Format all files (or only modified files if -g is passed)
|
||||
@@ -13,7 +21,9 @@ fi
|
||||
# --modified: files tracked by git that have been modified (staged or unstaged)
|
||||
# --exclude-standard: ignores files in .gitignore
|
||||
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
||||
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
|
||||
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| xargs -r clang-format -style=file -i
|
||||
|
||||
@@ -45,22 +45,9 @@ byte arrays, and emits headers under
|
||||
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
|
||||
in flash.
|
||||
|
||||
To refresh the firmware assets after updating the `.bin` files, run:
|
||||
A convenient script `update_hyphenation.sh` is used to update all languages.
|
||||
To use it, run:
|
||||
|
||||
```
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/en.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/de.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
|
||||
```sh
|
||||
./scripts/update_hypenation.sh
|
||||
```
|
||||
|
||||
229
docs/i18n.md
Normal file
229
docs/i18n.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Internationalization (I18N)
|
||||
|
||||
This guide explains the multi-language support system in CrossPoint Reader.
|
||||
|
||||
## Supported Languages (Updating)
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
### Translation System Architecture
|
||||
|
||||
The I18N system uses **per-language YAML files** to maintain translations and a Python script to generate C++ code:
|
||||
|
||||
```
|
||||
lib/I18n/
|
||||
├── translations/ # One YAML file per language
|
||||
│ ├── english.yaml
|
||||
│ ├── spanish.yaml
|
||||
│ ├── french.yaml
|
||||
│ └── ...
|
||||
├── I18n.h
|
||||
├── I18n.cpp
|
||||
├── I18nKeys.h # Enums (auto-generated)
|
||||
├── I18nStrings.h # String array declarations (auto-generated)
|
||||
└── I18nStrings.cpp # String array definitions (auto-generated)
|
||||
|
||||
scripts/
|
||||
└── gen_i18n.py # Code generator script
|
||||
```
|
||||
|
||||
**Key principle:** All translations are managed in the YAML files under `lib/I18n/translations/`. The Python script generates the necessary C++ code automatically.
|
||||
|
||||
---
|
||||
|
||||
### YAML File Format
|
||||
|
||||
Each language has its own file in `lib/I18n/translations/` (e.g. `spanish.yaml`).
|
||||
|
||||
A file looks like this:
|
||||
|
||||
```yaml
|
||||
_language_name: "Español"
|
||||
_language_code: "SPANISH"
|
||||
_order: "1"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "BOOTING"
|
||||
STR_BROWSE_FILES: "Buscar archivos"
|
||||
```
|
||||
|
||||
**Metadata keys** (prefixed with `_`):
|
||||
- `_language_name` — Native display name shown to the user (e.g. "Français")
|
||||
- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier.
|
||||
- `_order` — Controls the position in the Language enum (English is always 0)
|
||||
|
||||
**Rules:**
|
||||
- Use UTF-8 encoding
|
||||
- Every line must follow the format: `KEY: "value"`
|
||||
- Keys must be valid C++ identifiers (uppercase, strats with STR_)
|
||||
- Keys must be unique within a file
|
||||
- String values must be quoted
|
||||
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
|
||||
|
||||
---
|
||||
|
||||
### Adding New Strings
|
||||
|
||||
To add a new translatable string:
|
||||
|
||||
#### 1. Edit the English YAML file
|
||||
|
||||
Add the key to `lib/I18n/translations/english.yaml`:
|
||||
|
||||
```yaml
|
||||
STR_MY_NEW_STRING: "My New String"
|
||||
```
|
||||
|
||||
Then add translations in each language file. If a key is missing from a
|
||||
language file, the generator will automatically use the English text as a
|
||||
fallback (and print a warning).
|
||||
|
||||
#### 2. Run the generator script
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically:
|
||||
- Fills missing translations from English
|
||||
- Updates the `StrId` enum in `I18nKeys.h`
|
||||
- Regenerates all language arrays in `I18nStrings.cpp`
|
||||
|
||||
#### 3. Use in code
|
||||
|
||||
```cpp
|
||||
#include <I18n.h>
|
||||
|
||||
// Using the tr() macro (recommended)
|
||||
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
|
||||
|
||||
// Using I18N.get() directly
|
||||
const char* text = I18N.get(StrId::STR_MY_NEW_STRING);
|
||||
```
|
||||
|
||||
**That's it!** No manual array synchronization needed.
|
||||
|
||||
---
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
To add support for a new language (e.g., Italian):
|
||||
|
||||
#### 1. Create a new YAML file
|
||||
|
||||
Create `lib/I18n/translations/italian.yaml`:
|
||||
|
||||
```yaml
|
||||
_language_name: "Italiano"
|
||||
_language_code: "ITALIAN"
|
||||
_order: "7"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "AVVIO"
|
||||
```
|
||||
|
||||
You only need to include the strings you have translations for. Missing
|
||||
keys will fall back to English automatically.
|
||||
|
||||
#### 2. Run the generator
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically updates all necessary code.
|
||||
|
||||
---
|
||||
|
||||
### Modifying Existing Translations
|
||||
|
||||
Simply edit the relevant YAML file and regenerate:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UTF-8 Encoding
|
||||
|
||||
The YAML files use UTF-8 encoding. Special characters are automatically converted to C++ UTF-8 hex sequences by the generator.
|
||||
|
||||
---
|
||||
|
||||
### I18N API Reference
|
||||
|
||||
```cpp
|
||||
// === Convenience Macros (Recommended) ===
|
||||
|
||||
// tr(id) - Get translated string without StrId:: prefix
|
||||
const char* text = tr(STR_SETTINGS_TITLE);
|
||||
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
|
||||
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
|
||||
|
||||
// I18N - Shorthand for I18n::getInstance()
|
||||
I18N.setLanguage(Language::SPANISH);
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// === Full API ===
|
||||
|
||||
// Get the singleton instance
|
||||
I18n& instance = I18n::getInstance();
|
||||
|
||||
// Get translated string (three equivalent ways)
|
||||
const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
|
||||
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
||||
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||
|
||||
// Set language
|
||||
I18N.setLanguage(Language::SPANISH);
|
||||
|
||||
// Get current language
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// Save language setting to file
|
||||
I18N.saveSettings();
|
||||
|
||||
// Load language setting from file
|
||||
I18N.loadSettings();
|
||||
|
||||
// Get character set for font subsetting (static method)
|
||||
const char* chars = I18n::getCharacterSet(Language::FRENCH);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Storage
|
||||
|
||||
Language settings are stored in:
|
||||
```
|
||||
/.crosspoint/language.bin
|
||||
```
|
||||
|
||||
This file contains:
|
||||
- Version byte
|
||||
- Current language selection (1 byte)
|
||||
|
||||
---
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
### For Developers (Adding Features)
|
||||
|
||||
1. Add new strings to `lib/I18n/translations/english.yaml`
|
||||
2. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
|
||||
3. Use the new `StrId` in your code
|
||||
4. Request translations from translators
|
||||
|
||||
### For Translators
|
||||
|
||||
1. Open the YAML file for your language in `lib/I18n/translations/`
|
||||
2. Add or update translations using the format `STR_KEY: "translated text"`
|
||||
3. Keep translations concise (E-ink space constraints)
|
||||
4. Make sure the file is in UTF-8 encoding
|
||||
5. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/` to verify
|
||||
6. Test on device or submit for review
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 54 KiB |
27
docs/translators.md
Normal file
27
docs/translators.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Translators
|
||||
|
||||
Below is a list of users and languages CrossPoint may support in the future.
|
||||
Note because a language is below does not mean there is official support for the language at this time.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to add your name to this list, please open a PR adding yourself and your Github link. Thank you!
|
||||
|
||||
## French
|
||||
- [Spigaw](https://github.com/Spigaw)
|
||||
|
||||
## German
|
||||
- [DavidOrtmann](https://github.com/DavidOrtmann)
|
||||
|
||||
## Italian
|
||||
- [fragolinux](https://github.com/fragolinux)
|
||||
|
||||
## Russian
|
||||
- [madebyKir](https://github.com/madebyKir)
|
||||
|
||||
## Spanish
|
||||
- [yeyeto2788](https://github.com/yeyeto2788)
|
||||
- [Skrzakk](https://github.com/Skrzakk)
|
||||
|
||||
## Swedish
|
||||
- [dawiik](https://github.com/dawiik)
|
||||
@@ -1,14 +1,14 @@
|
||||
# Web Server Guide
|
||||
|
||||
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone.
|
||||
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload files from your computer or phone.
|
||||
|
||||
## Overview
|
||||
|
||||
CrossPoint Reader includes a built-in web server that allows you to:
|
||||
|
||||
- Upload EPUB files wirelessly from any device on the same WiFi network
|
||||
- Upload files wirelessly from any device on the same WiFi network
|
||||
- Browse and manage files on your device's SD card
|
||||
- Create folders to organize your ebooks
|
||||
- Create folders to organize your library
|
||||
- Delete files and folders
|
||||
|
||||
## Prerequisites
|
||||
@@ -129,34 +129,31 @@ Click **File Manager** to access file management features.
|
||||
#### Browsing Files
|
||||
|
||||
- The file manager displays all files and folders on your SD card
|
||||
- **Folders** are highlighted in yellow with a 📁 icon
|
||||
- **EPUB files** are highlighted in green with a 📗 icon
|
||||
- **Folders** are highlighted in yellow and indicated with a 📁 icon
|
||||
- **EPUB Files** are highlighted in green and indicated with a 📗 icon
|
||||
- **All Other Files** are not highlighted and indicated with a 📄 icon
|
||||
- Click on a folder name to navigate into it
|
||||
- Use the breadcrumb navigation at the top to go back to parent folders
|
||||
|
||||
<img src="./images/wifi/webserver_files.png" width="600">
|
||||
|
||||
#### Uploading EPUB Files
|
||||
#### Uploading Files
|
||||
|
||||
1. Click the **+ Add** button in the top-right corner
|
||||
2. Select **Upload eBook** from the dropdown menu
|
||||
3. Click **Choose File** and select an `.epub` file from your device
|
||||
4. Click **Upload**
|
||||
5. A progress bar will show the upload status
|
||||
6. The page will automatically refresh when the upload is complete
|
||||
|
||||
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
|
||||
1. Click the **📤 Upload** button in the top-right corner
|
||||
2. Click **Choose File** and select a file from your device
|
||||
3. Click **Upload**
|
||||
4. A progress bar will show the upload status
|
||||
5. The page will automatically refresh when the upload is complete
|
||||
|
||||
<img src="./images/wifi/webserver_upload.png" width="600">
|
||||
|
||||
#### Creating Folders
|
||||
|
||||
1. Click the **+ Add** button in the top-right corner
|
||||
2. Select **New Folder** from the dropdown menu
|
||||
3. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
4. Click **Create Folder**
|
||||
1. Click the **📁 New Folder** button in the top-right corner
|
||||
2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
3. Click **Create Folder**
|
||||
|
||||
This is useful for organizing your ebooks by genre, author, or series.
|
||||
This is useful for organizing your library by genre, author, series or file type.
|
||||
|
||||
#### Deleting Files and Folders
|
||||
|
||||
@@ -168,11 +165,25 @@ This is useful for organizing your ebooks by genre, author, or series.
|
||||
|
||||
**Note:** Folders must be empty before they can be deleted.
|
||||
|
||||
#### Moving Files
|
||||
|
||||
1. Click the **📂** (folder) icon next to any file
|
||||
2. Enter a folder name or select one from the dropdown
|
||||
3. Click **Move** to relocate the file
|
||||
|
||||
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
|
||||
|
||||
#### Renaming Files
|
||||
|
||||
1. Click the **✏️** (pencil) icon next to any file
|
||||
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
3. Click **Rename** to permanently rename the file
|
||||
|
||||
---
|
||||
|
||||
## Command Line File Management
|
||||
|
||||
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md).
|
||||
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. Detailed documentation can be found [here](./webserver-endpoints.md).
|
||||
|
||||
## Security Notes
|
||||
|
||||
@@ -189,7 +200,6 @@ For power users, you can manage files directly from your terminal using `curl` w
|
||||
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
|
||||
- **Web Server Port:** 80 (HTTP)
|
||||
- **Maximum Upload Size:** Limited by available SD card space
|
||||
- **Supported File Format:** `.epub` only
|
||||
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
---
|
||||
@@ -198,7 +208,7 @@ For power users, you can manage files directly from your terminal using `curl` w
|
||||
|
||||
1. **Organize with folders** - Create folders before uploading to keep your library organized
|
||||
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
|
||||
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload
|
||||
3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
|
||||
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
|
||||
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
|
||||
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef OMIT_BOOKERLY
|
||||
#include <builtinFonts/bookerly_12_bold.h>
|
||||
#include <builtinFonts/bookerly_12_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_12_italic.h>
|
||||
@@ -16,7 +17,10 @@
|
||||
#include <builtinFonts/bookerly_18_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_18_italic.h>
|
||||
#include <builtinFonts/bookerly_18_regular.h>
|
||||
#endif // OMIT_BOOKERLY
|
||||
|
||||
#include <builtinFonts/notosans_8_regular.h>
|
||||
#ifndef OMIT_NOTOSANS
|
||||
#include <builtinFonts/notosans_12_bold.h>
|
||||
#include <builtinFonts/notosans_12_bolditalic.h>
|
||||
#include <builtinFonts/notosans_12_italic.h>
|
||||
@@ -33,6 +37,9 @@
|
||||
#include <builtinFonts/notosans_18_bolditalic.h>
|
||||
#include <builtinFonts/notosans_18_italic.h>
|
||||
#include <builtinFonts/notosans_18_regular.h>
|
||||
#endif // OMIT_NOTOSANS
|
||||
|
||||
#ifndef OMIT_OPENDYSLEXIC
|
||||
#include <builtinFonts/opendyslexic_10_bold.h>
|
||||
#include <builtinFonts/opendyslexic_10_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_10_italic.h>
|
||||
@@ -49,6 +56,8 @@
|
||||
#include <builtinFonts/opendyslexic_8_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_8_italic.h>
|
||||
#include <builtinFonts/opendyslexic_8_regular.h>
|
||||
#endif // OMIT_OPENDYSLEXIC
|
||||
|
||||
#include <builtinFonts/ubuntu_10_bold.h>
|
||||
#include <builtinFonts/ubuntu_10_regular.h>
|
||||
#include <builtinFonts/ubuntu_12_bold.h>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,6 @@ class Epub {
|
||||
bool parseTocNcxFile() const;
|
||||
bool parseTocNavFile() const;
|
||||
void parseCssFiles() const;
|
||||
std::string getCssRulesCache() const;
|
||||
bool loadCssRulesFromCache() const;
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
@@ -54,10 +52,23 @@ class Epub {
|
||||
const std::string& getAuthor() const;
|
||||
const std::string& getLanguage() const;
|
||||
std::string getCoverBmpPath(bool cropped = false) const;
|
||||
// Generate a 1-bit BMP cover image from the EPUB cover image.
|
||||
// Returns true on success. On conversion failure, callers may use
|
||||
// `generateInvalidFormatCoverBmp` to create a valid marker BMP.
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
// Create a valid 1-bit BMP that visually indicates an invalid/unsupported
|
||||
// cover format (an X pattern). This prevents repeated generation attempts
|
||||
// by providing a valid BMP file that `isValidThumbnailBmp` accepts.
|
||||
bool generateInvalidFormatCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
// Generate a thumbnail BMP at the requested `height`. Returns true on
|
||||
// successful conversion. If conversion fails, `generateInvalidFormatThumbBmp`
|
||||
// can be used to write a valid marker image that prevents retries.
|
||||
bool generateThumbBmp(int height) const;
|
||||
// Create a valid 1-bit thumbnail BMP with an X marker indicating an
|
||||
// invalid/unsupported cover image instead of leaving an empty marker file.
|
||||
bool generateInvalidFormatThumbBmp(int height) const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
@@ -73,5 +84,10 @@ class Epub {
|
||||
|
||||
size_t getBookSize() const;
|
||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
const CssParser* getCssParser() const { return cssParser.get(); }
|
||||
CssParser* getCssParser() const { return cssParser.get(); }
|
||||
|
||||
static bool isValidThumbnailBmp(const std::string& bmpPath);
|
||||
|
||||
private:
|
||||
std::vector<std::string> getCoverCandidates() const;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "BookMetadataCache.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
@@ -21,15 +21,15 @@ bool BookMetadataCache::beginWrite() {
|
||||
buildMode = true;
|
||||
spineCount = 0;
|
||||
tocCount = 0;
|
||||
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
||||
LOG_DBG("BMC", "Entering write mode");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginContentOpfPass() {
|
||||
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
||||
LOG_DBG("BMC", "Beginning content opf pass");
|
||||
|
||||
// Open spine file for writing
|
||||
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endContentOpfPass() {
|
||||
@@ -38,12 +38,12 @@ bool BookMetadataCache::endContentOpfPass() {
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||
LOG_DBG("BMC", "Beginning toc pass");
|
||||
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
spineFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ bool BookMetadataCache::beginTocPass() {
|
||||
});
|
||||
spineFile.seek(0);
|
||||
useSpineHrefIndex = true;
|
||||
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
|
||||
LOG_DBG("BMC", "Using fast index for %d spine items", spineCount);
|
||||
} else {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
@@ -87,27 +87,27 @@ bool BookMetadataCache::endTocPass() {
|
||||
|
||||
bool BookMetadataCache::endWrite() {
|
||||
if (!buildMode) {
|
||||
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
|
||||
LOG_DBG("BMC", "endWrite called but not in build mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
buildMode = false;
|
||||
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||
// Open all three files, writing to meta, reading from spine and toc
|
||||
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
return false;
|
||||
@@ -167,7 +167,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
ZipFile zip(epubPath);
|
||||
// Pre-open zip file to speed up size calculations
|
||||
if (!zip.open()) {
|
||||
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis());
|
||||
LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
@@ -185,7 +185,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
bool useBatchSizes = false;
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
|
||||
LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
|
||||
|
||||
std::vector<ZipFile::SizeTarget> targets;
|
||||
targets.reserve(spineCount);
|
||||
@@ -208,7 +208,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
|
||||
spineSizes.resize(spineCount, 0);
|
||||
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
|
||||
LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", matched, spineCount);
|
||||
|
||||
targets.clear();
|
||||
targets.shrink_to_fit();
|
||||
@@ -227,9 +227,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||
// Logging here is for debugging
|
||||
if (spineEntry.tocIndex == -1) {
|
||||
Serial.printf(
|
||||
"[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s, using title from last section\n",
|
||||
millis(), i, spineEntry.href.c_str());
|
||||
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
|
||||
spineEntry.href.c_str());
|
||||
spineEntry.tocIndex = lastSpineTocIndex;
|
||||
}
|
||||
lastSpineTocIndex = spineEntry.tocIndex;
|
||||
@@ -240,13 +239,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
if (itemSize == 0) {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,16 +269,16 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
|
||||
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
||||
LOG_DBG("BMC", "Successfully built book.bin");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||
SdMan.remove((cachePath + tmpSpineBinFile).c_str());
|
||||
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||
Storage.remove((cachePath + tmpSpineBinFile).c_str());
|
||||
}
|
||||
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||
SdMan.remove((cachePath + tmpTocBinFile).c_str());
|
||||
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||
Storage.remove((cachePath + tmpTocBinFile).c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -306,7 +305,7 @@ uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) c
|
||||
// this is because in this function we're marking positions of the items
|
||||
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||
if (!buildMode || !spineFile) {
|
||||
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
|
||||
LOG_DBG("BMC", "createSpineEntry called but not in build mode");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,7 +317,7 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||
const uint8_t level) {
|
||||
if (!buildMode || !tocFile || !spineFile) {
|
||||
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
|
||||
LOG_DBG("BMC", "createTocEntry called but not in build mode");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -340,7 +339,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
}
|
||||
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||
}
|
||||
} else {
|
||||
spineFile.seek(0);
|
||||
@@ -352,7 +351,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
}
|
||||
}
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,14 +363,14 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||
|
||||
bool BookMetadataCache::load() {
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(bookFile, version);
|
||||
if (version != BOOK_CACHE_VERSION) {
|
||||
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
|
||||
LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -387,18 +386,18 @@ bool BookMetadataCache::load() {
|
||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||
|
||||
loaded = true;
|
||||
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||
if (!loaded) {
|
||||
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
|
||||
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
||||
LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -412,12 +411,12 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
if (!loaded) {
|
||||
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
||||
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
||||
LOG_ERR("BMC", "getTocEntry index %d out of range", index);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
// Cell padding in pixels (must match TABLE_CELL_PAD_* in ChapterHtmlSlimParser.cpp)
|
||||
static constexpr int TABLE_CELL_PADDING_X = 4;
|
||||
static constexpr int TABLE_CELL_PADDING_TOP = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageLine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
@@ -25,6 +34,142 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageImage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
// Images don't use fontId or text rendering
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageImage::serialize(FsFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
|
||||
// serialize ImageBlock
|
||||
return imageBlock->serialize(file);
|
||||
}
|
||||
|
||||
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||
int16_t xPos;
|
||||
int16_t yPos;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
|
||||
auto ib = ImageBlock::deserialize(file);
|
||||
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageTableRow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PageTableRow::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
const int baseX = xPos + xOffset;
|
||||
const int baseY = yPos + yOffset;
|
||||
|
||||
// Draw horizontal borders (top and bottom of this row)
|
||||
renderer.drawLine(baseX, baseY, baseX + totalWidth, baseY);
|
||||
renderer.drawLine(baseX, baseY + rowHeight, baseX + totalWidth, baseY + rowHeight);
|
||||
|
||||
// Draw vertical borders and render cell contents
|
||||
// Left edge
|
||||
renderer.drawLine(baseX, baseY, baseX, baseY + rowHeight);
|
||||
|
||||
for (const auto& cell : cells) {
|
||||
// Right vertical border for this cell
|
||||
const int cellRightX = baseX + cell.xOffset + cell.columnWidth;
|
||||
renderer.drawLine(cellRightX, baseY, cellRightX, baseY + rowHeight);
|
||||
|
||||
// Render each text line within the cell
|
||||
const int cellTextX = baseX + cell.xOffset + TABLE_CELL_PADDING_X;
|
||||
int cellLineY = baseY + 1 + TABLE_CELL_PADDING_TOP; // 1px border + top padding
|
||||
|
||||
for (const auto& line : cell.lines) {
|
||||
line->render(renderer, fontId, cellTextX, cellLineY);
|
||||
cellLineY += lineHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PageTableRow::serialize(FsFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
serialization::writePod(file, rowHeight);
|
||||
serialization::writePod(file, totalWidth);
|
||||
serialization::writePod(file, lineHeight);
|
||||
|
||||
const uint16_t cellCount = static_cast<uint16_t>(cells.size());
|
||||
serialization::writePod(file, cellCount);
|
||||
|
||||
for (const auto& cell : cells) {
|
||||
serialization::writePod(file, cell.xOffset);
|
||||
serialization::writePod(file, cell.columnWidth);
|
||||
|
||||
const uint16_t lineCount = static_cast<uint16_t>(cell.lines.size());
|
||||
serialization::writePod(file, lineCount);
|
||||
|
||||
for (const auto& line : cell.lines) {
|
||||
if (!line->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<PageTableRow> PageTableRow::deserialize(FsFile& file) {
|
||||
int16_t xPos, yPos, rowHeight, totalWidth, lineHeight;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
serialization::readPod(file, rowHeight);
|
||||
serialization::readPod(file, totalWidth);
|
||||
serialization::readPod(file, lineHeight);
|
||||
|
||||
uint16_t cellCount;
|
||||
serialization::readPod(file, cellCount);
|
||||
|
||||
// Sanity check
|
||||
if (cellCount > 100) {
|
||||
LOG_ERR("PTR", "Deserialization failed: cell count %u exceeds maximum", cellCount);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<PageTableCellData> cells;
|
||||
cells.resize(cellCount);
|
||||
|
||||
for (uint16_t c = 0; c < cellCount; ++c) {
|
||||
serialization::readPod(file, cells[c].xOffset);
|
||||
serialization::readPod(file, cells[c].columnWidth);
|
||||
|
||||
uint16_t lineCount;
|
||||
serialization::readPod(file, lineCount);
|
||||
|
||||
if (lineCount > 1000) {
|
||||
LOG_ERR("PTR", "Deserialization failed: line count %u in cell %u exceeds maximum", lineCount, c);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
cells[c].lines.reserve(lineCount);
|
||||
for (uint16_t l = 0; l < lineCount; ++l) {
|
||||
auto tb = TextBlock::deserialize(file);
|
||||
if (!tb) {
|
||||
return nullptr;
|
||||
}
|
||||
cells[c].lines.push_back(std::move(tb));
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<PageTableRow>(
|
||||
new PageTableRow(std::move(cells), rowHeight, totalWidth, lineHeight, xPos, yPos));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||
for (auto& element : elements) {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
@@ -36,8 +181,7 @@ bool Page::serialize(FsFile& file) const {
|
||||
serialization::writePod(file, count);
|
||||
|
||||
for (const auto& el : elements) {
|
||||
// Only PageLine exists currently
|
||||
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
||||
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||
if (!el->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -59,11 +203,68 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
if (tag == TAG_PageLine) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else if (tag == TAG_PageTableRow) {
|
||||
auto tr = PageTableRow::deserialize(file);
|
||||
if (!tr) {
|
||||
LOG_ERR("PGE", "Deserialization failed for PageTableRow at element %u", i);
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(tr));
|
||||
} else if (tag == TAG_PageImage) {
|
||||
auto pi = PageImage::deserialize(file);
|
||||
page->elements.push_back(std::move(pi));
|
||||
} else {
|
||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
bool Page::getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const {
|
||||
bool firstImage = true;
|
||||
for (const auto& el : elements) {
|
||||
if (el->getTag() == TAG_PageImage) {
|
||||
PageImage* pi = static_cast<PageImage*>(el.get());
|
||||
ImageBlock* ib = pi->getImageBlock();
|
||||
|
||||
if (firstImage) {
|
||||
// Initialize with first image bounds
|
||||
outX = pi->xPos;
|
||||
outY = pi->yPos;
|
||||
outWidth = ib->getWidth();
|
||||
outHeight = ib->getHeight();
|
||||
firstImage = false;
|
||||
} else {
|
||||
// Expand bounding box to include this image
|
||||
int imgX = pi->xPos;
|
||||
int imgY = pi->yPos;
|
||||
int imgW = ib->getWidth();
|
||||
int imgH = ib->getHeight();
|
||||
|
||||
// Expand right boundary
|
||||
if (imgX + imgW > outX + outWidth) {
|
||||
outWidth = (imgX + imgW) - outX;
|
||||
}
|
||||
// Expand left boundary
|
||||
if (imgX < outX) {
|
||||
int oldRight = outX + outWidth;
|
||||
outX = imgX;
|
||||
outWidth = oldRight - outX;
|
||||
}
|
||||
// Expand bottom boundary
|
||||
if (imgY + imgH > outY + outHeight) {
|
||||
outHeight = (imgY + imgH) - outY;
|
||||
}
|
||||
// Expand top boundary
|
||||
if (imgY < outY) {
|
||||
int oldBottom = outY + outHeight;
|
||||
outY = imgY;
|
||||
outHeight = oldBottom - outY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !firstImage; // Return true if at least one image was found
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/ImageBlock.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageTableRow = 2,
|
||||
TAG_PageImage = 3,
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
@@ -17,6 +21,7 @@ class PageElement {
|
||||
int16_t yPos;
|
||||
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||
virtual ~PageElement() = default;
|
||||
virtual PageElementTag getTag() const = 0;
|
||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||
virtual bool serialize(FsFile& file) = 0;
|
||||
};
|
||||
@@ -28,11 +33,59 @@ class PageLine final : public PageElement {
|
||||
public:
|
||||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
/// Data for a single cell within a PageTableRow.
|
||||
struct PageTableCellData {
|
||||
std::vector<std::shared_ptr<TextBlock>> lines; // Laid-out text lines for this cell
|
||||
uint16_t columnWidth = 0; // Width of this column in pixels
|
||||
uint16_t xOffset = 0; // X offset of this cell within the row
|
||||
};
|
||||
|
||||
/// A table row element that renders cells in a column-aligned grid with borders.
|
||||
class PageTableRow final : public PageElement {
|
||||
std::vector<PageTableCellData> cells;
|
||||
int16_t rowHeight; // Total row height in pixels
|
||||
int16_t totalWidth; // Total table width in pixels
|
||||
int16_t lineHeight; // Height of one text line (for vertical positioning of cell lines)
|
||||
|
||||
public:
|
||||
PageTableRow(std::vector<PageTableCellData> cells, int16_t rowHeight, int16_t totalWidth, int16_t lineHeight,
|
||||
int16_t xPos, int16_t yPos)
|
||||
: PageElement(xPos, yPos),
|
||||
cells(std::move(cells)),
|
||||
rowHeight(rowHeight),
|
||||
totalWidth(totalWidth),
|
||||
lineHeight(lineHeight) {}
|
||||
|
||||
int16_t getHeight() const { return rowHeight; }
|
||||
PageElementTag getTag() const override { return TAG_PageTableRow; }
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
static std::unique_ptr<PageTableRow> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
// An image element on a page
|
||||
class PageImage final : public PageElement {
|
||||
std::shared_ptr<ImageBlock> imageBlock;
|
||||
|
||||
public:
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||
|
||||
// Helper to get image block dimensions (needed for bounding box calculation)
|
||||
ImageBlock* getImageBlock() const { return imageBlock.get(); }
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
// the list of block index and line numbers on this page
|
||||
@@ -40,4 +93,15 @@ class Page {
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(FsFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||
|
||||
// Check if page contains any images (used to force full refresh)
|
||||
bool hasImages() const {
|
||||
return std::any_of(elements.begin(), elements.end(),
|
||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
// Get the bounding box of all images on this page.
|
||||
// Returns true if page has images and fills out the bounding box coordinates.
|
||||
// If no images, returns false.
|
||||
bool getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
@@ -32,6 +31,9 @@ void stripSoftHyphensInPlace(std::string& word) {
|
||||
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
||||
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
||||
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
||||
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
|
||||
return renderer.getSpaceWidth(fontId);
|
||||
}
|
||||
const bool hasSoftHyphen = containsSoftHyphen(word);
|
||||
if (!hasSoftHyphen && !appendHyphen) {
|
||||
return renderer.getTextWidth(fontId, word.c_str(), style);
|
||||
@@ -60,6 +62,13 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
}
|
||||
wordStyles.push_back(combinedStyle);
|
||||
wordContinues.push_back(attachToPrevious);
|
||||
forceBreakAfter.push_back(false);
|
||||
}
|
||||
|
||||
void ParsedText::addLineBreak() {
|
||||
if (!words.empty()) {
|
||||
forceBreakAfter.back() = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes data to minimize memory usage
|
||||
@@ -77,37 +86,26 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
// Build indexed continues vector from the parallel list for O(1) access during layout
|
||||
std::vector<bool> continuesVec(wordContinues.begin(), wordContinues.end());
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
if (hyphenationEnabled) {
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
}
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, continuesVec, lineBreakIndices, processLine);
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
||||
const size_t totalWordCount = words.size();
|
||||
|
||||
std::vector<uint16_t> wordWidths;
|
||||
wordWidths.reserve(totalWordCount);
|
||||
wordWidths.reserve(words.size());
|
||||
|
||||
auto wordsIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
|
||||
while (wordsIt != words.end()) {
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
||||
|
||||
std::advance(wordsIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, words[i], wordStyles[i]));
|
||||
}
|
||||
|
||||
return wordWidths;
|
||||
@@ -132,8 +130,7 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
// First word needs to fit in reduced width if there's an indent
|
||||
const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
while (wordWidths[i] > effectiveWidth) {
|
||||
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true,
|
||||
&continuesVec)) {
|
||||
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -158,6 +155,11 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// If the previous word has a forced line break, this line cannot include word j
|
||||
if (j > static_cast<size_t>(i) && !forceBreakAfter.empty() && forceBreakAfter[j - 1]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
const int gap = j > static_cast<size_t>(i) && !continuesVec[j] ? spaceWidth : 0;
|
||||
currlen += wordWidths[j] + gap;
|
||||
@@ -166,8 +168,11 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
break;
|
||||
}
|
||||
|
||||
// Cannot break after word j if the next word attaches to it (continuation group)
|
||||
if (j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||
// Forced line break after word j overrides continuation (must end line here)
|
||||
const bool mustBreakHere = !forceBreakAfter.empty() && forceBreakAfter[j];
|
||||
|
||||
// Cannot break after word j if the next word attaches to it (unless forced)
|
||||
if (!mustBreakHere && j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -190,6 +195,11 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
dp[i] = cost;
|
||||
ans[i] = j; // j is the index of the last word in this optimal line
|
||||
}
|
||||
|
||||
// After evaluating cost, enforce forced break - no more words on this line
|
||||
if (mustBreakHere) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle oversized word: if no valid configuration found, force single-word line
|
||||
@@ -264,6 +274,11 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
|
||||
// Consume as many words as possible for current line, splitting when prefixes fit
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
// If the previous word has a forced line break, stop - this word starts a new line
|
||||
if (currentIndex > lineStart && !forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
const int spacing = isFirstWord || continuesVec[currentIndex] ? 0 : spaceWidth;
|
||||
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||
@@ -272,6 +287,11 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
if (lineWidth + candidateWidth <= effectivePageWidth) {
|
||||
lineWidth += candidateWidth;
|
||||
++currentIndex;
|
||||
|
||||
// If the word we just added has a forced break, end this line now
|
||||
if (!forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -279,8 +299,8 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
const int availableWidth = effectivePageWidth - lineWidth - spacing;
|
||||
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
||||
|
||||
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
||||
allowFallbackBreaks, &continuesVec)) {
|
||||
if (availableWidth > 0 &&
|
||||
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
||||
// Prefix now fits; append it to this line and move to next line
|
||||
lineWidth += spacing + wordWidths[currentIndex];
|
||||
++currentIndex;
|
||||
@@ -297,7 +317,12 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
|
||||
// Don't break before a continuation word (e.g., orphaned "?" after "question").
|
||||
// Backtrack to the start of the continuation group so the whole group moves to the next line.
|
||||
// But don't backtrack past a forced break point.
|
||||
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
|
||||
// Don't backtrack past a forced break
|
||||
if (!forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
|
||||
break;
|
||||
}
|
||||
--currentIndex;
|
||||
}
|
||||
|
||||
@@ -312,20 +337,14 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
// available width.
|
||||
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
||||
const int fontId, std::vector<uint16_t>& wordWidths,
|
||||
const bool allowFallbackBreaks, std::vector<bool>* continuesVec) {
|
||||
const bool allowFallbackBreaks) {
|
||||
// Guard against invalid indices or zero available width before attempting to split.
|
||||
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get iterators to target word and style.
|
||||
auto wordIt = words.begin();
|
||||
auto styleIt = wordStyles.begin();
|
||||
std::advance(wordIt, wordIndex);
|
||||
std::advance(styleIt, wordIndex);
|
||||
|
||||
const std::string& word = *wordIt;
|
||||
const auto style = *styleIt;
|
||||
const std::string& word = words[wordIndex];
|
||||
const auto style = wordStyles[wordIndex];
|
||||
|
||||
// Collect candidate breakpoints (byte offsets and hyphen requirements).
|
||||
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
|
||||
@@ -362,31 +381,26 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
|
||||
// Split the word at the selected breakpoint and append a hyphen if required.
|
||||
std::string remainder = word.substr(chosenOffset);
|
||||
wordIt->resize(chosenOffset);
|
||||
words[wordIndex].resize(chosenOffset);
|
||||
if (chosenNeedsHyphen) {
|
||||
wordIt->push_back('-');
|
||||
words[wordIndex].push_back('-');
|
||||
}
|
||||
|
||||
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
|
||||
auto insertWordIt = std::next(wordIt);
|
||||
auto insertStyleIt = std::next(styleIt);
|
||||
words.insert(insertWordIt, remainder);
|
||||
wordStyles.insert(insertStyleIt, style);
|
||||
words.insert(words.begin() + wordIndex + 1, remainder);
|
||||
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
|
||||
|
||||
// The remainder inherits whatever continuation status the original word had with the word after it.
|
||||
// Find the continues entry for the original word and insert the remainder's entry after it.
|
||||
auto continuesIt = wordContinues.begin();
|
||||
std::advance(continuesIt, wordIndex);
|
||||
const bool originalContinuedToNext = *continuesIt;
|
||||
const bool originalContinuedToNext = wordContinues[wordIndex];
|
||||
// The original word (now prefix) does NOT continue to remainder (hyphen separates them)
|
||||
*continuesIt = false;
|
||||
const auto insertContinuesIt = std::next(continuesIt);
|
||||
wordContinues.insert(insertContinuesIt, originalContinuedToNext);
|
||||
wordContinues[wordIndex] = false;
|
||||
wordContinues.insert(wordContinues.begin() + wordIndex + 1, originalContinuedToNext);
|
||||
|
||||
// Keep the indexed vector in sync if provided
|
||||
if (continuesVec) {
|
||||
(*continuesVec)[wordIndex] = false;
|
||||
continuesVec->insert(continuesVec->begin() + wordIndex + 1, originalContinuedToNext);
|
||||
// Forced break belongs to the original whole word; transfer it to the remainder (last part).
|
||||
if (!forceBreakAfter.empty()) {
|
||||
const bool originalForceBreak = forceBreakAfter[wordIndex];
|
||||
forceBreakAfter[wordIndex] = false; // prefix doesn't force break
|
||||
forceBreakAfter.insert(forceBreakAfter.begin() + wordIndex + 1, originalForceBreak);
|
||||
}
|
||||
|
||||
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||
@@ -447,7 +461,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
// Continuation words attach to the previous word with no space before them
|
||||
std::list<uint16_t> lineXPos;
|
||||
std::vector<uint16_t> lineXPos;
|
||||
lineXPos.reserve(lineWordCount);
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
|
||||
@@ -460,23 +475,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing);
|
||||
}
|
||||
|
||||
// Iterators always start at the beginning as we are moving content with splice below
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
auto wordContinuesEndIt = wordContinues.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
std::advance(wordContinuesEndIt, lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
|
||||
// Consume continues flags (not passed to TextBlock, but must be consumed to stay in sync)
|
||||
std::list<bool> lineContinues;
|
||||
lineContinues.splice(lineContinues.begin(), wordContinues, wordContinues.begin(), wordContinuesEndIt);
|
||||
// Build line data by moving from the original vectors using index range
|
||||
std::vector<std::string> lineWords(std::make_move_iterator(words.begin() + lastBreakAt),
|
||||
std::make_move_iterator(words.begin() + lineBreak));
|
||||
std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
if (containsSoftHyphen(word)) {
|
||||
@@ -487,3 +489,22 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
processLine(
|
||||
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||
}
|
||||
|
||||
uint16_t ParsedText::getNaturalWidth(const GfxRenderer& renderer, const int fontId) const {
|
||||
if (words.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
int totalWidth = 0;
|
||||
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
totalWidth += measureWordWidth(renderer, fontId, words[i], wordStyles[i]);
|
||||
// Add a space before this word unless it's the first word or a continuation
|
||||
if (i > 0 && !wordContinues[i]) {
|
||||
totalWidth += spaceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<uint16_t>(std::min(totalWidth, static_cast<int>(UINT16_MAX)));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -14,9 +13,10 @@
|
||||
class GfxRenderer;
|
||||
|
||||
class ParsedText {
|
||||
std::list<std::string> words;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::list<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
std::vector<std::string> words;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
std::vector<bool> forceBreakAfter; // true = mandatory line break after this word (e.g. <br> in table cells)
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
@@ -28,8 +28,7 @@ class ParsedText {
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||
std::vector<bool>* continuesVec = nullptr);
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
@@ -42,6 +41,10 @@ class ParsedText {
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||
|
||||
/// Mark a forced line break after the last word (e.g. for <br> within table cells).
|
||||
/// If no words have been added yet, this is a no-op.
|
||||
void addLineBreak();
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||
size_t size() const { return words.size(); }
|
||||
@@ -49,4 +52,9 @@ class ParsedText {
|
||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
bool includeLastLine = true);
|
||||
|
||||
/// Returns the "natural" width of the content if it were laid out on a single line
|
||||
/// (sum of word widths + space widths between non-continuation words).
|
||||
/// Used by table layout to determine column widths before line-breaking.
|
||||
uint16_t getNaturalWidth(const GfxRenderer& renderer, int fontId) const;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Section.h"
|
||||
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "Page.h"
|
||||
@@ -8,7 +9,7 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||
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(uint32_t);
|
||||
@@ -16,16 +17,16 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) +
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
||||
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t position = file.position();
|
||||
if (!page->serialize(file)) {
|
||||
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
|
||||
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
|
||||
return 0;
|
||||
}
|
||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
||||
LOG_DBG("SCT", "Page %d processed", pageCount);
|
||||
|
||||
pageCount++;
|
||||
return position;
|
||||
@@ -36,7 +37,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool embeddedStyle) {
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
||||
LOG_DBG("SCT", "File not open for writing header");
|
||||
return;
|
||||
}
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
@@ -60,7 +61,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
bool Section::loadSectionFile(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) {
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, version);
|
||||
if (version != SECTION_FILE_VERSION) {
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
@@ -96,7 +97,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
@@ -104,23 +105,23 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
|
||||
serialization::readPod(file, pageCount);
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
||||
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||
bool Section::clearCache() const {
|
||||
if (!SdMan.exists(filePath.c_str())) {
|
||||
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
||||
if (!Storage.exists(filePath.c_str())) {
|
||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SdMan.remove(filePath.c_str())) {
|
||||
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
||||
if (!Storage.remove(filePath.c_str())) {
|
||||
LOG_ERR("SCT", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
||||
LOG_DBG("SCT", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -134,7 +135,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
// Create cache directory if it doesn't exist
|
||||
{
|
||||
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||
SdMan.mkdir(sectionsDir.c_str());
|
||||
Storage.mkdir(sectionsDir.c_str());
|
||||
}
|
||||
|
||||
// Retry logic for SD card timing issues
|
||||
@@ -142,17 +143,17 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
uint32_t fileSize = 0;
|
||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
||||
if (attempt > 0) {
|
||||
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
||||
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||
delay(50); // Brief delay before retry
|
||||
}
|
||||
|
||||
// Remove any incomplete file from previous attempt before retrying
|
||||
if (SdMan.exists(tmpHtmlPath.c_str())) {
|
||||
SdMan.remove(tmpHtmlPath.c_str());
|
||||
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
}
|
||||
|
||||
FsFile tmpHtml;
|
||||
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||
continue;
|
||||
}
|
||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||
@@ -160,39 +161,56 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
tmpHtml.close();
|
||||
|
||||
// If streaming failed, remove the incomplete file immediately
|
||||
if (!success && SdMan.exists(tmpHtmlPath.c_str())) {
|
||||
SdMan.remove(tmpHtmlPath.c_str());
|
||||
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
||||
if (!success && Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
||||
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
||||
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||
|
||||
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
||||
if (!Storage.openFileForWrite("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle);
|
||||
std::vector<uint32_t> lut = {};
|
||||
|
||||
// Derive the content base directory and image cache path prefix for the parser
|
||||
size_t lastSlash = localPath.find_last_of('/');
|
||||
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||
|
||||
CssParser* cssParser = nullptr;
|
||||
if (embeddedStyle) {
|
||||
cssParser = epub->getCssParser();
|
||||
if (cssParser) {
|
||||
if (!cssParser->loadFromCache()) {
|
||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
ChapterHtmlSlimParser visitor(
|
||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||
embeddedStyle, popupFn, embeddedStyle ? epub->getCssParser() : nullptr);
|
||||
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
SdMan.remove(tmpHtmlPath.c_str());
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
||||
LOG_ERR("SCT", "Failed to parse XML and build pages");
|
||||
file.close();
|
||||
SdMan.remove(filePath.c_str());
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -208,9 +226,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
|
||||
if (hasFailedLutRecords) {
|
||||
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
|
||||
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||
file.close();
|
||||
SdMan.remove(filePath.c_str());
|
||||
Storage.remove(filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -219,11 +237,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
serialization::writePod(file, pageCount);
|
||||
serialization::writePod(file, lutOffset);
|
||||
file.close();
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
29
lib/Epub/Epub/TableData.h
Normal file
29
lib/Epub/Epub/TableData.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ParsedText.h"
|
||||
#include "css/CssStyle.h"
|
||||
|
||||
/// A single cell in a table row.
|
||||
struct TableCell {
|
||||
std::unique_ptr<ParsedText> content;
|
||||
bool isHeader = false; // true for <th>, false for <td>
|
||||
int colspan = 1; // number of logical columns this cell spans
|
||||
CssLength widthHint; // width hint from HTML attribute or CSS (if hasWidthHint)
|
||||
bool hasWidthHint = false;
|
||||
};
|
||||
|
||||
/// A single row in a table.
|
||||
struct TableRow {
|
||||
std::vector<TableCell> cells;
|
||||
};
|
||||
|
||||
/// Buffered table data collected during SAX parsing.
|
||||
/// The entire table must be buffered before layout because column widths
|
||||
/// depend on content across all rows.
|
||||
struct TableData {
|
||||
std::vector<TableRow> rows;
|
||||
std::vector<CssLength> colWidthHints; // width hints from <col> tags, indexed by logical column
|
||||
};
|
||||
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||
class Block {
|
||||
public:
|
||||
virtual ~Block() = default;
|
||||
virtual void layout(GfxRenderer& renderer) = 0;
|
||||
|
||||
virtual BlockType getType() = 0;
|
||||
virtual bool isEmpty() = 0;
|
||||
virtual void finish() {}
|
||||
|
||||
175
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
175
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "../converters/DitherUtils.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
|
||||
// Cache file format:
|
||||
// - uint16_t width
|
||||
// - uint16_t height
|
||||
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||
|
||||
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||
: imagePath(imagePath), width(width), height(height) {}
|
||||
|
||||
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||
|
||||
namespace {
|
||||
|
||||
std::string getCachePath(const std::string& imagePath) {
|
||||
// Replace extension with .pxc (pixel cache)
|
||||
size_t dotPos = imagePath.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
return imagePath.substr(0, dotPos) + ".pxc";
|
||||
}
|
||||
return imagePath + ".pxc";
|
||||
}
|
||||
|
||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
FsFile cacheFile;
|
||||
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
Serial.printf("[%lu] [IMG] Cache dimension mismatch: %dx%d vs %dx%d\n", millis(), cachedWidth, cachedHeight,
|
||||
expectedWidth, expectedHeight);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||
expectedWidth = cachedWidth;
|
||||
expectedHeight = cachedHeight;
|
||||
|
||||
Serial.printf("[%lu] [IMG] Loading from cache: %s (%dx%d)\n", millis(), cachePath.c_str(), cachedWidth, cachedHeight);
|
||||
|
||||
// Read and render row by row to minimize memory usage
|
||||
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
if (!rowBuffer) {
|
||||
Serial.printf("[%lu] [IMG] Failed to allocate row buffer\n", millis());
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||
Serial.printf("[%lu] [IMG] Cache read error at row %d\n", millis(), row);
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int destY = y + row;
|
||||
for (int col = 0; col < cachedWidth; col++) {
|
||||
int byteIdx = col / 4;
|
||||
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
|
||||
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
||||
}
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
Serial.printf("[%lu] [IMG] Cache render complete\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
Serial.printf("[%lu] [IMG] Rendering image at %d,%d: %s (%dx%d)\n", millis(), x, y, imagePath.c_str(), width, height);
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Bounds check render position using logical screen dimensions
|
||||
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||
Serial.printf("[%lu] [IMG] Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)\n", millis(), x, y, width,
|
||||
height, screenWidth, screenHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
return; // Successfully rendered from cache
|
||||
}
|
||||
|
||||
// No cache - need to decode the image
|
||||
// Check if image file exists
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [IMG] Image file not found: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
file.close();
|
||||
|
||||
if (fileSize == 0) {
|
||||
Serial.printf("[%lu] [IMG] Image file is empty: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Decoding and caching: %s\n", millis(), imagePath.c_str());
|
||||
|
||||
RenderConfig config;
|
||||
config.x = x;
|
||||
config.y = y;
|
||||
config.maxWidth = width;
|
||||
config.maxHeight = height;
|
||||
config.useGrayscale = true;
|
||||
config.useDithering = true;
|
||||
config.performanceMode = false;
|
||||
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||
config.cachePath = cachePath; // Enable caching during decode
|
||||
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
Serial.printf("[%lu] [IMG] No decoder found for image: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Using %s decoder\n", millis(), decoder->getFormatName());
|
||||
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [IMG] Failed to decode image: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Decode successful\n", millis());
|
||||
}
|
||||
|
||||
bool ImageBlock::serialize(FsFile& file) {
|
||||
serialization::writeString(file, imagePath);
|
||||
serialization::writePod(file, width);
|
||||
serialization::writePod(file, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
||||
std::string path;
|
||||
serialization::readString(file, path);
|
||||
int16_t w, h;
|
||||
serialization::readPod(file, w);
|
||||
serialization::readPod(file, h);
|
||||
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||
}
|
||||
31
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
31
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Block.h"
|
||||
|
||||
class ImageBlock final : public Block {
|
||||
public:
|
||||
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
||||
~ImageBlock() override = default;
|
||||
|
||||
const std::string& getImagePath() const { return imagePath; }
|
||||
int16_t getWidth() const { return width; }
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
bool imageExists() const;
|
||||
|
||||
BlockType getType() override { return IMAGE_BLOCK; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
void render(GfxRenderer& renderer, const int x, const int y);
|
||||
bool serialize(FsFile& file);
|
||||
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
||||
|
||||
private:
|
||||
std::string imagePath;
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
@@ -1,26 +1,24 @@
|
||||
#include "TextBlock.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||
// Validate iterator bounds before rendering
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
|
||||
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||
return;
|
||||
}
|
||||
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
const int wordX = *wordXposIt + x;
|
||||
const EpdFontFamily::Style currentStyle = *wordStylesIt;
|
||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, currentStyle);
|
||||
const int wordX = wordXpos[i] + x;
|
||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
||||
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
||||
|
||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = *wordIt;
|
||||
const std::string& w = words[i];
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||
@@ -40,17 +38,13 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
|
||||
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
||||
}
|
||||
|
||||
std::advance(wordIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
std::advance(wordXposIt, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool TextBlock::serialize(FsFile& file) const {
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
||||
words.size(), wordXpos.size(), wordStyles.size());
|
||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
|
||||
wordXpos.size(), wordStyles.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,17 +73,17 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
|
||||
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
uint16_t wc;
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
serialization::readPod(file, wc);
|
||||
|
||||
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
||||
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
||||
if (wc > 10000) {
|
||||
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
|
||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
#include <EpdFontFamily.h>
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
@@ -12,14 +12,14 @@
|
||||
// Represents a line of text on a page
|
||||
class TextBlock final : public Block {
|
||||
private:
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||
std::list<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
|
||||
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
@@ -27,8 +27,10 @@ class TextBlock final : public Block {
|
||||
~TextBlock() override = default;
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
const std::vector<std::string>& getWords() const { return words; }
|
||||
const std::vector<uint16_t>& getWordXpos() const { return wordXpos; }
|
||||
const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
void layout(GfxRenderer& renderer) override {};
|
||||
// given a renderer works out where to break the words into lines
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
BlockType getType() override { return TEXT_BLOCK; }
|
||||
|
||||
40
lib/Epub/Epub/converters/DitherUtils.h
Normal file
40
lib/Epub/Epub/converters/DitherUtils.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 4x4 Bayer matrix for ordered dithering
|
||||
inline const uint8_t bayer4x4[4][4] = {
|
||||
{0, 8, 2, 10},
|
||||
{12, 4, 14, 6},
|
||||
{3, 11, 1, 9},
|
||||
{15, 7, 13, 5},
|
||||
};
|
||||
|
||||
// Apply Bayer dithering and quantize to 4 levels (0-3)
|
||||
// Stateless - works correctly with any pixel processing order
|
||||
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||
int bayer = bayer4x4[y & 3][x & 3];
|
||||
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
|
||||
|
||||
int adjusted = gray + dither;
|
||||
if (adjusted < 0) adjusted = 0;
|
||||
if (adjusted > 255) adjusted = 255;
|
||||
|
||||
if (adjusted < 64) return 0;
|
||||
if (adjusted < 128) return 1;
|
||||
if (adjusted < 192) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Draw a pixel respecting the current render mode for grayscale support
|
||||
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
|
||||
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
|
||||
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
|
||||
renderer.drawPixel(x, y, true);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
|
||||
renderer.drawPixel(x, y, false);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
|
||||
renderer.drawPixel(x, y, false);
|
||||
}
|
||||
}
|
||||
42
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
42
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "ImageDecoderFactory.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||
|
||||
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||
std::string ext = imagePath;
|
||||
size_t dotPos = ext.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
ext = ext.substr(dotPos);
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
} else {
|
||||
ext = "";
|
||||
}
|
||||
|
||||
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!jpegDecoder) {
|
||||
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||
}
|
||||
return jpegDecoder.get();
|
||||
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!pngDecoder) {
|
||||
pngDecoder.reset(new PngToFramebufferConverter());
|
||||
}
|
||||
return pngDecoder.get();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [DEC] No decoder found for image: %s\n", millis(), imagePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||
20
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
20
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class JpegToFramebufferConverter;
|
||||
class PngToFramebufferConverter;
|
||||
|
||||
class ImageDecoderFactory {
|
||||
public:
|
||||
// Returns non-owning pointer - factory owns the decoder lifetime
|
||||
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
|
||||
static bool isFormatSupported(const std::string& imagePath);
|
||||
|
||||
private:
|
||||
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
|
||||
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
|
||||
};
|
||||
18
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
18
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||
if (width * height > MAX_SOURCE_PIXELS) {
|
||||
Serial.printf("[%lu] [IMG] Image too large (%dx%d = %d pixels %s), max supported: %d pixels\n", millis(), width,
|
||||
height, width * height, format.c_str(), MAX_SOURCE_PIXELS);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||
Serial.printf("[%lu] [IMG] Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.\n",
|
||||
millis(), feature.c_str(), imagePath.c_str());
|
||||
}
|
||||
40
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
40
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
struct ImageDimensions {
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
|
||||
struct RenderConfig {
|
||||
int x, y;
|
||||
int maxWidth, maxHeight;
|
||||
bool useGrayscale = true;
|
||||
bool useDithering = true;
|
||||
bool performanceMode = false;
|
||||
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
|
||||
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
|
||||
};
|
||||
|
||||
class ImageToFramebufferDecoder {
|
||||
public:
|
||||
virtual ~ImageToFramebufferDecoder() = default;
|
||||
|
||||
virtual bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) = 0;
|
||||
|
||||
virtual bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const = 0;
|
||||
|
||||
virtual const char* getFormatName() const = 0;
|
||||
|
||||
protected:
|
||||
// Size validation helpers
|
||||
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
||||
|
||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||
};
|
||||
298
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
298
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,298 @@
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
#include <picojpeg.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
struct JpegContext {
|
||||
FsFile& file;
|
||||
uint8_t buffer[512];
|
||||
size_t bufferPos;
|
||||
size_t bufferFilled;
|
||||
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
|
||||
};
|
||||
|
||||
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open file for dimensions: %s\n", millis(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JpegContext context(file);
|
||||
pjpeg_image_info_t imageInfo;
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
file.close();
|
||||
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] Failed to init JPEG for dimensions: %d\n", millis(), status);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = imageInfo.m_width;
|
||||
out.height = imageInfo.m_height;
|
||||
Serial.printf("[%lu] [JPG] Image dimensions: %dx%d\n", millis(), out.width, out.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
Serial.printf("[%lu] [JPG] Decoding JPEG: %s\n", millis(), imagePath.c_str());
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open file: %s\n", millis(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JpegContext context(file);
|
||||
pjpeg_image_info_t imageInfo;
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] picojpeg init failed: %d\n", millis(), status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate output dimensions
|
||||
int destWidth, destHeight;
|
||||
float scale;
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||
destWidth = config.maxWidth;
|
||||
destHeight = config.maxHeight;
|
||||
scale = (float)destWidth / imageInfo.m_width;
|
||||
} else {
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth)
|
||||
? (float)config.maxWidth / imageInfo.m_width
|
||||
: 1.0f;
|
||||
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
|
||||
? (float)config.maxHeight / imageInfo.m_height
|
||||
: 1.0f;
|
||||
scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (scale > 1.0f) scale = 1.0f;
|
||||
|
||||
destWidth = (int)(imageInfo.m_width * scale);
|
||||
destHeight = (int)(imageInfo.m_height * scale);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [JPG] JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d\n", millis(),
|
||||
imageInfo.m_width, imageInfo.m_height, destWidth, destHeight, scale, imageInfo.m_scanType,
|
||||
imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
||||
|
||||
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
|
||||
Serial.printf("[%lu] [JPG] Null buffer pointers in imageInfo\n", millis());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Allocate pixel cache if cachePath is provided
|
||||
PixelCache cache;
|
||||
bool caching = !config.cachePath.empty();
|
||||
if (caching) {
|
||||
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to allocate cache buffer, continuing without caching\n", millis());
|
||||
caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
int mcuX = 0;
|
||||
int mcuY = 0;
|
||||
|
||||
while (mcuY < imageInfo.m_MCUSPerCol) {
|
||||
status = pjpeg_decode_mcu();
|
||||
if (status == PJPG_NO_MORE_BLOCKS) {
|
||||
break;
|
||||
}
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] MCU decode failed: %d\n", millis(), status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Source position in image coordinates
|
||||
int srcStartX = mcuX * imageInfo.m_MCUWidth;
|
||||
int srcStartY = mcuY * imageInfo.m_MCUHeight;
|
||||
|
||||
switch (imageInfo.m_scanType) {
|
||||
case PJPG_GRAYSCALE:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH1V1:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH2V1:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 16; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockIndex = (col < 8) ? 0 : 1;
|
||||
int pixelIndex = row * 8 + (col % 8);
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH1V2:
|
||||
for (int row = 0; row < 16; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockIndex = (row < 8) ? 0 : 1;
|
||||
int pixelIndex = (row % 8) * 8 + col;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH2V2:
|
||||
for (int row = 0; row < 16; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 16; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockX = (col < 8) ? 0 : 1;
|
||||
int blockY = (row < 8) ? 0 : 1;
|
||||
int blockIndex = blockY * 2 + blockX;
|
||||
int pixelIndex = (row % 8) * 8 + (col % 8);
|
||||
int blockOffset = blockIndex * 64;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
mcuX++;
|
||||
if (mcuX >= imageInfo.m_MCUSPerRow) {
|
||||
mcuX = 0;
|
||||
mcuY++;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [JPG] Decoding complete\n", millis());
|
||||
file.close();
|
||||
|
||||
// Write cache file if caching was enabled
|
||||
if (caching) {
|
||||
cache.writeToFile(config.cachePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
||||
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
|
||||
|
||||
if (context->bufferPos >= context->bufferFilled) {
|
||||
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
|
||||
if (readCount <= 0) {
|
||||
*pBytes_actually_read = 0;
|
||||
return 0;
|
||||
}
|
||||
context->bufferFilled = readCount;
|
||||
context->bufferPos = 0;
|
||||
}
|
||||
|
||||
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
|
||||
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
|
||||
|
||||
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
|
||||
context->bufferPos += bytesToCopy;
|
||||
*pBytes_actually_read = bytesToCopy;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".jpg" || ext == ".jpeg");
|
||||
}
|
||||
24
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
24
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||
public:
|
||||
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||
|
||||
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||
|
||||
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||
return getDimensionsStatic(imagePath, dims);
|
||||
}
|
||||
|
||||
static bool supportsFormat(const std::string& extension);
|
||||
const char* getFormatName() const override { return "JPEG"; }
|
||||
|
||||
private:
|
||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||
};
|
||||
85
lib/Epub/Epub/converters/PixelCache.h
Normal file
85
lib/Epub/Epub/converters/PixelCache.h
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Cache buffer for storing 2-bit pixels (4 levels) during decode.
|
||||
// Packs 4 pixels per byte, MSB first.
|
||||
struct PixelCache {
|
||||
uint8_t* buffer;
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
int originX; // config.x - to convert screen coords to cache coords
|
||||
int originY; // config.y
|
||||
|
||||
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
|
||||
PixelCache(const PixelCache&) = delete;
|
||||
PixelCache& operator=(const PixelCache&) = delete;
|
||||
|
||||
static constexpr size_t MAX_CACHE_BYTES = 256 * 1024; // 256KB limit for embedded targets
|
||||
|
||||
bool allocate(int w, int h, int ox, int oy) {
|
||||
width = w;
|
||||
height = h;
|
||||
originX = ox;
|
||||
originY = oy;
|
||||
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
size_t bufferSize = (size_t)bytesPerRow * h;
|
||||
if (bufferSize > MAX_CACHE_BYTES) {
|
||||
Serial.printf("[%lu] [IMG] Cache buffer too large: %d bytes for %dx%d (limit %d)\n", millis(), bufferSize, w, h,
|
||||
MAX_CACHE_BYTES);
|
||||
return false;
|
||||
}
|
||||
buffer = (uint8_t*)malloc(bufferSize);
|
||||
if (buffer) {
|
||||
memset(buffer, 0, bufferSize);
|
||||
Serial.printf("[%lu] [IMG] Allocated cache buffer: %d bytes for %dx%d\n", millis(), bufferSize, w, h);
|
||||
}
|
||||
return buffer != nullptr;
|
||||
}
|
||||
|
||||
void setPixel(int screenX, int screenY, uint8_t value) {
|
||||
if (!buffer) return;
|
||||
int localX = screenX - originX;
|
||||
int localY = screenY - originY;
|
||||
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
|
||||
|
||||
int byteIdx = localY * bytesPerRow + localX / 4;
|
||||
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
|
||||
bool writeToFile(const std::string& cachePath) {
|
||||
if (!buffer) return false;
|
||||
|
||||
FsFile cacheFile;
|
||||
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||
Serial.printf("[%lu] [IMG] Failed to open cache file for writing: %s\n", millis(), cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t w = width;
|
||||
uint16_t h = height;
|
||||
cacheFile.write(&w, 2);
|
||||
cacheFile.write(&h, 2);
|
||||
cacheFile.write(buffer, bytesPerRow * height);
|
||||
cacheFile.close();
|
||||
|
||||
Serial.printf("[%lu] [IMG] Cache written: %s (%dx%d, %d bytes)\n", millis(), cachePath.c_str(), width, height,
|
||||
4 + bytesPerRow * height);
|
||||
return true;
|
||||
}
|
||||
|
||||
~PixelCache() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
364
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
364
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <PNGdec.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Context struct passed through PNGdec callbacks to avoid global mutable state.
|
||||
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
|
||||
struct PngContext {
|
||||
GfxRenderer* renderer;
|
||||
const RenderConfig* config;
|
||||
int screenWidth;
|
||||
int screenHeight;
|
||||
|
||||
// Scaling state
|
||||
float scale;
|
||||
int srcWidth;
|
||||
int srcHeight;
|
||||
int dstWidth;
|
||||
int dstHeight;
|
||||
int lastDstY; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
PixelCache cache;
|
||||
bool caching;
|
||||
|
||||
uint8_t* grayLineBuffer;
|
||||
|
||||
PngContext()
|
||||
: renderer(nullptr),
|
||||
config(nullptr),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
scale(1.0f),
|
||||
srcWidth(0),
|
||||
srcHeight(0),
|
||||
dstWidth(0),
|
||||
dstHeight(0),
|
||||
lastDstY(-1),
|
||||
caching(false),
|
||||
grayLineBuffer(nullptr) {}
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the FsFile*,
|
||||
// avoiding the need for global file state.
|
||||
void* pngOpenWithHandle(const char* filename, int32_t* size) {
|
||||
FsFile* f = new FsFile();
|
||||
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
|
||||
delete f;
|
||||
return nullptr;
|
||||
}
|
||||
*size = f->size();
|
||||
return f;
|
||||
}
|
||||
|
||||
void pngCloseWithHandle(void* handle) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(handle);
|
||||
if (f) {
|
||||
f->close();
|
||||
delete f;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||
if (!f) return 0;
|
||||
return f->read(pBuf, len);
|
||||
}
|
||||
|
||||
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||
if (!f) return -1;
|
||||
return f->seek(pos);
|
||||
}
|
||||
|
||||
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
|
||||
// We heap-allocate it on demand rather than using a static instance, so this memory
|
||||
// is only consumed while actually decoding/querying PNG images. This is critical on
|
||||
// the ESP32-C3 where total RAM is ~320 KB.
|
||||
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||
|
||||
// Convert entire source line to grayscale with alpha blending to white background.
|
||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
memcpy(grayLine, pPixels, width);
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_INDEXED:
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = pPixels[x];
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &palette[pPixels[x] * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t gray = pPixels[x * 2];
|
||||
uint8_t alpha = pPixels[x * 2 + 1];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 4];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
memset(grayLine, 128, width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * ctx->scale);
|
||||
|
||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||
if (dstY == ctx->lastDstY) return 1;
|
||||
ctx->lastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= ctx->dstHeight) return 1;
|
||||
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) return 1;
|
||||
|
||||
// Convert entire source line to grayscale (improves cache locality)
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||
pDraw->iHasAlpha);
|
||||
|
||||
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||
int dstWidth = ctx->dstWidth;
|
||||
int outXBase = ctx->config->x;
|
||||
int screenWidth = ctx->screenWidth;
|
||||
bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
Serial.printf("[%lu] [PNG] Not enough heap for PNG decoder (%u free, need %u)\n", millis(), freeHeap,
|
||||
MIN_FREE_HEAP_FOR_PNG);
|
||||
return false;
|
||||
}
|
||||
|
||||
PNG* png = new (std::nothrow) PNG();
|
||||
if (!png) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate PNG decoder for dimensions\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
nullptr);
|
||||
|
||||
if (rc != 0) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open PNG for dimensions: %d\n", millis(), rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = png->getWidth();
|
||||
out.height = png->getHeight();
|
||||
|
||||
png->close();
|
||||
delete png;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
Serial.printf("[%lu] [PNG] Decoding PNG: %s\n", millis(), imagePath.c_str());
|
||||
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
Serial.printf("[%lu] [PNG] Not enough heap for PNG decoder (%u free, need %u)\n", millis(), freeHeap,
|
||||
MIN_FREE_HEAP_FOR_PNG);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
|
||||
PNG* png = new (std::nothrow) PNG();
|
||||
if (!png) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate PNG decoder\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
PngContext ctx;
|
||||
ctx.renderer = &renderer;
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
ctx.screenHeight = renderer.getScreenHeight();
|
||||
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
pngDrawCallback);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open PNG: %d\n", millis(), rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate output dimensions
|
||||
ctx.srcWidth = png->getWidth();
|
||||
ctx.srcHeight = png->getHeight();
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||
ctx.dstWidth = config.maxWidth;
|
||||
ctx.dstHeight = config.maxHeight;
|
||||
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||
} else {
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||
|
||||
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
Serial.printf("[%lu] [PNG] PNG %dx%d -> %dx%d (scale %.2f), bpp: %d\n", millis(), ctx.srcWidth, ctx.srcHeight,
|
||||
ctx.dstWidth, ctx.dstHeight, ctx.scale, png->getBpp());
|
||||
|
||||
if (png->getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||
}
|
||||
|
||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate gray line buffer\n", millis());
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate cache buffer using SCALED dimensions
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate cache buffer, continuing without caching\n", millis());
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long decodeStart = millis();
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
free(ctx.grayLineBuffer);
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("[%lu] [PNG] Decode failed: %d\n", millis(), rc);
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
png->close();
|
||||
delete png;
|
||||
Serial.printf("[%lu] [PNG] PNG decoding complete - render time: %lu ms\n", millis(), decodeTime);
|
||||
|
||||
// Write cache file if caching was enabled and buffer was allocated
|
||||
if (ctx.caching) {
|
||||
ctx.cache.writeToFile(config.cachePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".png");
|
||||
}
|
||||
17
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
17
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class PngToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||
public:
|
||||
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||
|
||||
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||
|
||||
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||
return getDimensionsStatic(imagePath, dims);
|
||||
}
|
||||
|
||||
static bool supportsFormat(const std::string& extension);
|
||||
const char* getFormatName() const override { return "PNG"; }
|
||||
};
|
||||
@@ -1,144 +1,57 @@
|
||||
#include "CssParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Arduino.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
// Stack-allocated string buffer to avoid heap reallocations during parsing
|
||||
// Provides string-like interface with fixed capacity
|
||||
struct StackBuffer {
|
||||
static constexpr size_t CAPACITY = 1024;
|
||||
char data[CAPACITY];
|
||||
size_t len = 0;
|
||||
|
||||
void push_back(char c) {
|
||||
if (len < CAPACITY - 1) {
|
||||
data[len++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
void clear() { len = 0; }
|
||||
bool empty() const { return len == 0; }
|
||||
size_t size() const { return len; }
|
||||
|
||||
// Get string view of current content (zero-copy)
|
||||
std::string_view view() const { return std::string_view(data, len); }
|
||||
|
||||
// Convert to string for passing to functions (single allocation)
|
||||
std::string str() const { return std::string(data, len); }
|
||||
};
|
||||
|
||||
// Buffer size for reading CSS files
|
||||
constexpr size_t READ_BUFFER_SIZE = 512;
|
||||
|
||||
// Maximum CSS file size we'll process (prevent memory issues)
|
||||
constexpr size_t MAX_CSS_SIZE = 64 * 1024;
|
||||
// Maximum number of CSS rules to store in the selector map
|
||||
// Prevents unbounded memory growth from pathological CSS files
|
||||
constexpr size_t MAX_RULES = 1500;
|
||||
|
||||
// Minimum free heap required to apply CSS during rendering
|
||||
// If below this threshold, we skip CSS to avoid display artifacts.
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
|
||||
|
||||
// Maximum length for a single selector string
|
||||
// Prevents parsing of extremely long or malformed selectors
|
||||
constexpr size_t MAX_SELECTOR_LENGTH = 256;
|
||||
|
||||
// Check if character is CSS whitespace
|
||||
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||
|
||||
// Read entire file into string (with size limit)
|
||||
std::string readFileContent(FsFile& file) {
|
||||
std::string content;
|
||||
content.reserve(std::min(static_cast<size_t>(file.size()), MAX_CSS_SIZE));
|
||||
|
||||
char buffer[READ_BUFFER_SIZE];
|
||||
while (file.available() && content.size() < MAX_CSS_SIZE) {
|
||||
const int bytesRead = file.read(buffer, sizeof(buffer));
|
||||
if (bytesRead <= 0) break;
|
||||
content.append(buffer, bytesRead);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Remove CSS comments (/* ... */) from content
|
||||
std::string stripComments(const std::string& css) {
|
||||
std::string result;
|
||||
result.reserve(css.size());
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < css.size()) {
|
||||
// Look for start of comment
|
||||
if (pos + 1 < css.size() && css[pos] == '/' && css[pos + 1] == '*') {
|
||||
// Find end of comment
|
||||
const size_t endPos = css.find("*/", pos + 2);
|
||||
if (endPos == std::string::npos) {
|
||||
// Unterminated comment - skip rest of file
|
||||
break;
|
||||
}
|
||||
pos = endPos + 2;
|
||||
} else {
|
||||
result.push_back(css[pos]);
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Skip @-rules (like @media, @import, @font-face)
|
||||
// Returns position after the @-rule
|
||||
size_t skipAtRule(const std::string& css, const size_t start) {
|
||||
// Find the end - either semicolon (simple @-rule) or matching brace
|
||||
size_t pos = start + 1; // Skip the '@'
|
||||
|
||||
// Skip identifier
|
||||
while (pos < css.size() && (std::isalnum(css[pos]) || css[pos] == '-')) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Look for { or ;
|
||||
int braceDepth = 0;
|
||||
while (pos < css.size()) {
|
||||
const char c = css[pos];
|
||||
if (c == '{') {
|
||||
++braceDepth;
|
||||
} else if (c == '}') {
|
||||
--braceDepth;
|
||||
if (braceDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
} else if (c == ';' && braceDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
return css.size();
|
||||
}
|
||||
|
||||
// Extract next rule from CSS content
|
||||
// Returns true if a rule was found, with selector and body filled
|
||||
bool extractNextRule(const std::string& css, size_t& pos, std::string& selector, std::string& body) {
|
||||
selector.clear();
|
||||
body.clear();
|
||||
|
||||
// Skip whitespace and @-rules until we find a regular rule
|
||||
while (pos < css.size()) {
|
||||
// Skip whitespace
|
||||
while (pos < css.size() && isCssWhitespace(css[pos])) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
if (pos >= css.size()) return false;
|
||||
|
||||
// Handle @-rules iteratively (avoids recursion/stack overflow)
|
||||
if (css[pos] == '@') {
|
||||
pos = skipAtRule(css, pos);
|
||||
continue; // Try again after skipping the @-rule
|
||||
}
|
||||
|
||||
break; // Found start of a regular rule
|
||||
}
|
||||
|
||||
if (pos >= css.size()) return false;
|
||||
|
||||
// Find opening brace
|
||||
const size_t bracePos = css.find('{', pos);
|
||||
if (bracePos == std::string::npos) return false;
|
||||
|
||||
// Extract selector (everything before the brace)
|
||||
selector = css.substr(pos, bracePos - pos);
|
||||
|
||||
// Find matching closing brace
|
||||
int depth = 1;
|
||||
const size_t bodyStart = bracePos + 1;
|
||||
size_t bodyEnd = bodyStart;
|
||||
|
||||
while (bodyEnd < css.size() && depth > 0) {
|
||||
if (css[bodyEnd] == '{')
|
||||
++depth;
|
||||
else if (css[bodyEnd] == '}')
|
||||
--depth;
|
||||
++bodyEnd;
|
||||
}
|
||||
|
||||
// Extract body (between braces)
|
||||
if (bodyEnd > bodyStart) {
|
||||
body = css.substr(bodyStart, bodyEnd - bodyStart - 1);
|
||||
}
|
||||
|
||||
pos = bodyEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// String utilities implementation
|
||||
@@ -167,6 +80,28 @@ std::string CssParser::normalized(const std::string& s) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void CssParser::normalizedInto(const std::string& s, std::string& out) {
|
||||
out.clear();
|
||||
out.reserve(s.size());
|
||||
|
||||
bool inSpace = true; // Start true to skip leading space
|
||||
for (const char c : s) {
|
||||
if (isCssWhitespace(c)) {
|
||||
if (!inSpace) {
|
||||
out.push_back(' ');
|
||||
inSpace = true;
|
||||
}
|
||||
} else {
|
||||
out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!out.empty() && out.back() == ' ') {
|
||||
out.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> CssParser::splitOnChar(const std::string& s, const char delimiter) {
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
@@ -290,129 +225,98 @@ CssLength CssParser::interpretLength(const std::string& val) {
|
||||
|
||||
return CssLength{numericValue, unit};
|
||||
}
|
||||
|
||||
int8_t CssParser::interpretSpacing(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
if (v.empty()) return 0;
|
||||
|
||||
// For spacing, we convert to "lines" (discrete units for e-ink)
|
||||
// 1em ≈ 1 line, percentages based on ~30 lines per page
|
||||
|
||||
float multiplier = 0.0f;
|
||||
size_t unitStart = v.size();
|
||||
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
const char c = v[i];
|
||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||
unitStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string numPart = v.substr(0, unitStart);
|
||||
const std::string unitPart = v.substr(unitStart);
|
||||
|
||||
if (unitPart == "em" || unitPart == "rem") {
|
||||
multiplier = 1.0f; // 1em = 1 line
|
||||
} else if (unitPart == "%") {
|
||||
multiplier = 0.3f; // ~30 lines per page, so 10% = 3 lines
|
||||
} else {
|
||||
return 0; // Unsupported unit for spacing
|
||||
}
|
||||
|
||||
char* endPtr = nullptr;
|
||||
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
||||
|
||||
if (endPtr == numPart.c_str()) return 0;
|
||||
|
||||
int lines = static_cast<int>(numericValue * multiplier);
|
||||
|
||||
// Clamp to reasonable range (0-2 lines)
|
||||
if (lines < 0) lines = 0;
|
||||
if (lines > 2) lines = 2;
|
||||
|
||||
return static_cast<int8_t>(lines);
|
||||
}
|
||||
|
||||
// Declaration parsing
|
||||
|
||||
void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf,
|
||||
std::string& propValueBuf) {
|
||||
const size_t colonPos = decl.find(':');
|
||||
if (colonPos == std::string::npos || colonPos == 0) return;
|
||||
|
||||
normalizedInto(decl.substr(0, colonPos), propNameBuf);
|
||||
normalizedInto(decl.substr(colonPos + 1), propValueBuf);
|
||||
|
||||
if (propNameBuf.empty() || propValueBuf.empty()) return;
|
||||
|
||||
if (propNameBuf == "text-align") {
|
||||
style.textAlign = interpretAlignment(propValueBuf);
|
||||
style.defined.textAlign = 1;
|
||||
} else if (propNameBuf == "font-style") {
|
||||
style.fontStyle = interpretFontStyle(propValueBuf);
|
||||
style.defined.fontStyle = 1;
|
||||
} else if (propNameBuf == "font-weight") {
|
||||
style.fontWeight = interpretFontWeight(propValueBuf);
|
||||
style.defined.fontWeight = 1;
|
||||
} else if (propNameBuf == "text-decoration" || propNameBuf == "text-decoration-line") {
|
||||
style.textDecoration = interpretDecoration(propValueBuf);
|
||||
style.defined.textDecoration = 1;
|
||||
} else if (propNameBuf == "text-indent") {
|
||||
style.textIndent = interpretLength(propValueBuf);
|
||||
style.defined.textIndent = 1;
|
||||
} else if (propNameBuf == "margin-top") {
|
||||
style.marginTop = interpretLength(propValueBuf);
|
||||
style.defined.marginTop = 1;
|
||||
} else if (propNameBuf == "margin-bottom") {
|
||||
style.marginBottom = interpretLength(propValueBuf);
|
||||
style.defined.marginBottom = 1;
|
||||
} else if (propNameBuf == "margin-left") {
|
||||
style.marginLeft = interpretLength(propValueBuf);
|
||||
style.defined.marginLeft = 1;
|
||||
} else if (propNameBuf == "margin-right") {
|
||||
style.marginRight = interpretLength(propValueBuf);
|
||||
style.defined.marginRight = 1;
|
||||
} else if (propNameBuf == "margin") {
|
||||
const auto values = splitWhitespace(propValueBuf);
|
||||
if (!values.empty()) {
|
||||
style.marginTop = interpretLength(values[0]);
|
||||
style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop;
|
||||
style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop;
|
||||
style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight;
|
||||
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (propNameBuf == "padding-top") {
|
||||
style.paddingTop = interpretLength(propValueBuf);
|
||||
style.defined.paddingTop = 1;
|
||||
} else if (propNameBuf == "padding-bottom") {
|
||||
style.paddingBottom = interpretLength(propValueBuf);
|
||||
style.defined.paddingBottom = 1;
|
||||
} else if (propNameBuf == "padding-left") {
|
||||
style.paddingLeft = interpretLength(propValueBuf);
|
||||
style.defined.paddingLeft = 1;
|
||||
} else if (propNameBuf == "padding-right") {
|
||||
style.paddingRight = interpretLength(propValueBuf);
|
||||
style.defined.paddingRight = 1;
|
||||
} else if (propNameBuf == "padding") {
|
||||
const auto values = splitWhitespace(propValueBuf);
|
||||
if (!values.empty()) {
|
||||
style.paddingTop = interpretLength(values[0]);
|
||||
style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop;
|
||||
style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop;
|
||||
style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight;
|
||||
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
|
||||
1;
|
||||
}
|
||||
} else if (propNameBuf == "width") {
|
||||
style.width = interpretLength(propValueBuf);
|
||||
style.defined.width = 1;
|
||||
}
|
||||
}
|
||||
|
||||
CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
CssStyle style;
|
||||
std::string propNameBuf;
|
||||
std::string propValueBuf;
|
||||
|
||||
// Split declarations by semicolon
|
||||
const auto declarations = splitOnChar(declBlock, ';');
|
||||
|
||||
for (const auto& decl : declarations) {
|
||||
// Find colon separator
|
||||
const size_t colonPos = decl.find(':');
|
||||
if (colonPos == std::string::npos || colonPos == 0) continue;
|
||||
|
||||
std::string propName = normalized(decl.substr(0, colonPos));
|
||||
std::string propValue = normalized(decl.substr(colonPos + 1));
|
||||
|
||||
if (propName.empty() || propValue.empty()) continue;
|
||||
|
||||
// Match property and set value
|
||||
if (propName == "text-align") {
|
||||
style.textAlign = interpretAlignment(propValue);
|
||||
style.defined.textAlign = 1;
|
||||
} else if (propName == "font-style") {
|
||||
style.fontStyle = interpretFontStyle(propValue);
|
||||
style.defined.fontStyle = 1;
|
||||
} else if (propName == "font-weight") {
|
||||
style.fontWeight = interpretFontWeight(propValue);
|
||||
style.defined.fontWeight = 1;
|
||||
} else if (propName == "text-decoration" || propName == "text-decoration-line") {
|
||||
style.textDecoration = interpretDecoration(propValue);
|
||||
style.defined.textDecoration = 1;
|
||||
} else if (propName == "text-indent") {
|
||||
style.textIndent = interpretLength(propValue);
|
||||
style.defined.textIndent = 1;
|
||||
} else if (propName == "margin-top") {
|
||||
style.marginTop = interpretLength(propValue);
|
||||
style.defined.marginTop = 1;
|
||||
} else if (propName == "margin-bottom") {
|
||||
style.marginBottom = interpretLength(propValue);
|
||||
style.defined.marginBottom = 1;
|
||||
} else if (propName == "margin-left") {
|
||||
style.marginLeft = interpretLength(propValue);
|
||||
style.defined.marginLeft = 1;
|
||||
} else if (propName == "margin-right") {
|
||||
style.marginRight = interpretLength(propValue);
|
||||
style.defined.marginRight = 1;
|
||||
} else if (propName == "margin") {
|
||||
// Shorthand: 1-4 values for top, right, bottom, left
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (!values.empty()) {
|
||||
style.marginTop = interpretLength(values[0]);
|
||||
style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop;
|
||||
style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop;
|
||||
style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight;
|
||||
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (propName == "padding-top") {
|
||||
style.paddingTop = interpretLength(propValue);
|
||||
style.defined.paddingTop = 1;
|
||||
} else if (propName == "padding-bottom") {
|
||||
style.paddingBottom = interpretLength(propValue);
|
||||
style.defined.paddingBottom = 1;
|
||||
} else if (propName == "padding-left") {
|
||||
style.paddingLeft = interpretLength(propValue);
|
||||
style.defined.paddingLeft = 1;
|
||||
} else if (propName == "padding-right") {
|
||||
style.paddingRight = interpretLength(propValue);
|
||||
style.defined.paddingRight = 1;
|
||||
} else if (propName == "padding") {
|
||||
// Shorthand: 1-4 values for top, right, bottom, left
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (!values.empty()) {
|
||||
style.paddingTop = interpretLength(values[0]);
|
||||
style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop;
|
||||
style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop;
|
||||
style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight;
|
||||
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom =
|
||||
style.defined.paddingLeft = 1;
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= declBlock.size(); ++i) {
|
||||
if (i == declBlock.size() || declBlock[i] == ';') {
|
||||
if (i > start) {
|
||||
const size_t len = i - start;
|
||||
std::string decl = declBlock.substr(start, len);
|
||||
if (!decl.empty()) {
|
||||
parseDeclarationIntoStyle(decl, style, propNameBuf, propValueBuf);
|
||||
}
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,20 +325,33 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
|
||||
// Rule processing
|
||||
|
||||
void CssParser::processRuleBlock(const std::string& selectorGroup, const std::string& declarations) {
|
||||
const CssStyle style = parseDeclarations(declarations);
|
||||
|
||||
// Only store if any properties were set
|
||||
if (!style.defined.anySet()) return;
|
||||
void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style) {
|
||||
// Check if we've reached the rule limit before processing
|
||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle comma-separated selectors
|
||||
const auto selectors = splitOnChar(selectorGroup, ',');
|
||||
|
||||
for (const auto& sel : selectors) {
|
||||
// Validate selector length before processing
|
||||
if (sel.size() > MAX_SELECTOR_LENGTH) {
|
||||
LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize the selector
|
||||
std::string key = normalized(sel);
|
||||
if (key.empty()) continue;
|
||||
|
||||
// Skip if this would exceed the rule limit
|
||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store or merge with existing
|
||||
auto it = rulesBySelector_.find(key);
|
||||
if (it != rulesBySelector_.end()) {
|
||||
@@ -449,34 +366,162 @@ void CssParser::processRuleBlock(const std::string& selectorGroup, const std::st
|
||||
|
||||
bool CssParser::loadFromStream(FsFile& source) {
|
||||
if (!source) {
|
||||
Serial.printf("[%lu] [CSS] Cannot read from invalid file\n", millis());
|
||||
LOG_ERR("CSS", "Cannot read from invalid file");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const std::string content = readFileContent(source);
|
||||
if (content.empty()) {
|
||||
return true; // Empty file is valid
|
||||
size_t totalRead = 0;
|
||||
|
||||
// Use stack-allocated buffers for parsing to avoid heap reallocations
|
||||
StackBuffer selector;
|
||||
StackBuffer declBuffer;
|
||||
// Keep these as std::string since they're passed by reference to parseDeclarationIntoStyle
|
||||
std::string propNameBuf;
|
||||
std::string propValueBuf;
|
||||
|
||||
bool inComment = false;
|
||||
bool maybeSlash = false;
|
||||
bool prevStar = false;
|
||||
|
||||
bool inAtRule = false;
|
||||
int atDepth = 0;
|
||||
|
||||
int bodyDepth = 0;
|
||||
bool skippingRule = false;
|
||||
CssStyle currentStyle;
|
||||
|
||||
auto handleChar = [&](const char c) {
|
||||
if (inAtRule) {
|
||||
if (c == '{') {
|
||||
++atDepth;
|
||||
} else if (c == '}') {
|
||||
if (atDepth > 0) --atDepth;
|
||||
if (atDepth == 0) inAtRule = false;
|
||||
} else if (c == ';' && atDepth == 0) {
|
||||
inAtRule = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bodyDepth == 0) {
|
||||
if (selector.empty() && isCssWhitespace(c)) {
|
||||
return;
|
||||
}
|
||||
if (c == '@' && selector.empty()) {
|
||||
inAtRule = true;
|
||||
atDepth = 0;
|
||||
return;
|
||||
}
|
||||
if (c == '{') {
|
||||
bodyDepth = 1;
|
||||
currentStyle = CssStyle{};
|
||||
declBuffer.clear();
|
||||
if (selector.size() > MAX_SELECTOR_LENGTH * 4) {
|
||||
skippingRule = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
selector.push_back(c);
|
||||
return;
|
||||
}
|
||||
|
||||
// bodyDepth > 0
|
||||
if (c == '{') {
|
||||
++bodyDepth;
|
||||
return;
|
||||
}
|
||||
if (c == '}') {
|
||||
--bodyDepth;
|
||||
if (bodyDepth == 0) {
|
||||
if (!skippingRule && !declBuffer.empty()) {
|
||||
parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf);
|
||||
}
|
||||
if (!skippingRule) {
|
||||
processRuleBlockWithStyle(selector.str(), currentStyle);
|
||||
}
|
||||
selector.clear();
|
||||
declBuffer.clear();
|
||||
skippingRule = false;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (bodyDepth > 1) {
|
||||
return;
|
||||
}
|
||||
if (!skippingRule) {
|
||||
if (c == ';') {
|
||||
if (!declBuffer.empty()) {
|
||||
parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf);
|
||||
declBuffer.clear();
|
||||
}
|
||||
} else {
|
||||
declBuffer.push_back(c);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
char buffer[READ_BUFFER_SIZE];
|
||||
while (source.available()) {
|
||||
int bytesRead = source.read(buffer, sizeof(buffer));
|
||||
if (bytesRead <= 0) break;
|
||||
|
||||
totalRead += static_cast<size_t>(bytesRead);
|
||||
|
||||
for (int i = 0; i < bytesRead; ++i) {
|
||||
const char c = buffer[i];
|
||||
|
||||
if (inComment) {
|
||||
if (prevStar && c == '/') {
|
||||
inComment = false;
|
||||
prevStar = false;
|
||||
continue;
|
||||
}
|
||||
prevStar = c == '*';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (maybeSlash) {
|
||||
if (c == '*') {
|
||||
inComment = true;
|
||||
maybeSlash = false;
|
||||
prevStar = false;
|
||||
continue;
|
||||
}
|
||||
handleChar('/');
|
||||
maybeSlash = false;
|
||||
// fall through to process current char
|
||||
}
|
||||
|
||||
if (c == '/') {
|
||||
maybeSlash = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
handleChar(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove comments
|
||||
const std::string cleaned = stripComments(content);
|
||||
|
||||
// Parse rules
|
||||
size_t pos = 0;
|
||||
std::string selector, body;
|
||||
|
||||
while (extractNextRule(cleaned, pos, selector, body)) {
|
||||
processRuleBlock(selector, body);
|
||||
if (maybeSlash) {
|
||||
handleChar('/');
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Parsed %zu rules\n", millis(), rulesBySelector_.size());
|
||||
LOG_DBG("CSS", "Parsed %zu rules from %zu bytes", rulesBySelector_.size(), totalRead);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Style resolution
|
||||
|
||||
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
|
||||
static bool lowHeapWarningLogged = false;
|
||||
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
|
||||
if (!lowHeapWarningLogged) {
|
||||
lowHeapWarningLogged = true;
|
||||
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
|
||||
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
|
||||
}
|
||||
return CssStyle{};
|
||||
}
|
||||
CssStyle result;
|
||||
const std::string tag = normalized(tagName);
|
||||
|
||||
@@ -521,9 +566,17 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
|
||||
|
||||
// Cache format version - increment when format changes
|
||||
constexpr uint8_t CSS_CACHE_VERSION = 2;
|
||||
constexpr char rulesCache[] = "/css_rules.cache";
|
||||
|
||||
bool CssParser::saveToCache(FsFile& file) const {
|
||||
if (!file) {
|
||||
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
||||
|
||||
bool CssParser::saveToCache() const {
|
||||
if (cachePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -582,12 +635,18 @@ bool CssParser::saveToCache(FsFile& file) const {
|
||||
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Saved %u rules to cache\n", millis(), ruleCount);
|
||||
LOG_DBG("CSS", "Saved %u rules to cache", ruleCount);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CssParser::loadFromCache(FsFile& file) {
|
||||
if (!file) {
|
||||
bool CssParser::loadFromCache() {
|
||||
if (cachePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -597,13 +656,15 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
// Read and verify version
|
||||
uint8_t version = 0;
|
||||
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) {
|
||||
Serial.printf("[%lu] [CSS] Cache version mismatch (got %u, expected %u)\n", millis(), version, CSS_CACHE_VERSION);
|
||||
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read rule count
|
||||
uint16_t ruleCount = 0;
|
||||
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -613,6 +674,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
uint16_t selectorLen = 0;
|
||||
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -620,6 +682,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
selector.resize(selectorLen);
|
||||
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -629,24 +692,28 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
style.textAlign = static_cast<CssTextAlign>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
style.fontStyle = static_cast<CssFontStyle>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
style.fontWeight = static_cast<CssFontWeight>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
||||
@@ -668,6 +735,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
|
||||
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -675,6 +743,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
uint16_t definedBits = 0;
|
||||
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
style.defined.textAlign = (definedBits & 1 << 0) != 0;
|
||||
@@ -694,6 +763,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
rulesBySelector_[selector] = style;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Loaded %u rules from cache\n", millis(), ruleCount);
|
||||
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "CssStyle.h"
|
||||
@@ -29,7 +30,7 @@
|
||||
*/
|
||||
class CssParser {
|
||||
public:
|
||||
CssParser() = default;
|
||||
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||
~CssParser() = default;
|
||||
|
||||
// Non-copyable
|
||||
@@ -76,28 +77,35 @@ class CssParser {
|
||||
*/
|
||||
void clear() { rulesBySelector_.clear(); }
|
||||
|
||||
/**
|
||||
* Check if CSS rules cache file exists
|
||||
*/
|
||||
bool hasCache() const;
|
||||
|
||||
/**
|
||||
* Save parsed CSS rules to a cache file.
|
||||
* @param file Open file handle to write to
|
||||
* @return true if cache was written successfully
|
||||
*/
|
||||
bool saveToCache(FsFile& file) const;
|
||||
bool saveToCache() const;
|
||||
|
||||
/**
|
||||
* Load CSS rules from a cache file.
|
||||
* Clears any existing rules before loading.
|
||||
* @param file Open file handle to read from
|
||||
* @return true if cache was loaded successfully
|
||||
*/
|
||||
bool loadFromCache(FsFile& file);
|
||||
bool loadFromCache();
|
||||
|
||||
private:
|
||||
// Storage: maps normalized selector -> style properties
|
||||
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
||||
|
||||
std::string cachePath;
|
||||
|
||||
// Internal parsing helpers
|
||||
void processRuleBlock(const std::string& selectorGroup, const std::string& declarations);
|
||||
void processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style);
|
||||
static CssStyle parseDeclarations(const std::string& declBlock);
|
||||
static void parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf,
|
||||
std::string& propValueBuf);
|
||||
|
||||
// Individual property value parsers
|
||||
static CssTextAlign interpretAlignment(const std::string& val);
|
||||
@@ -105,10 +113,10 @@ class CssParser {
|
||||
static CssFontWeight interpretFontWeight(const std::string& val);
|
||||
static CssTextDecoration interpretDecoration(const std::string& val);
|
||||
static CssLength interpretLength(const std::string& val);
|
||||
static int8_t interpretSpacing(const std::string& val);
|
||||
|
||||
// String utilities
|
||||
static std::string normalized(const std::string& s);
|
||||
static void normalizedInto(const std::string& s, std::string& out);
|
||||
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
|
||||
static std::vector<std::string> splitWhitespace(const std::string& s);
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ struct CssPropertyFlags {
|
||||
uint16_t paddingBottom : 1;
|
||||
uint16_t paddingLeft : 1;
|
||||
uint16_t paddingRight : 1;
|
||||
uint16_t width : 1;
|
||||
|
||||
CssPropertyFlags()
|
||||
: textAlign(0),
|
||||
@@ -83,17 +84,19 @@ struct CssPropertyFlags {
|
||||
paddingTop(0),
|
||||
paddingBottom(0),
|
||||
paddingLeft(0),
|
||||
paddingRight(0) {}
|
||||
paddingRight(0),
|
||||
width(0) {}
|
||||
|
||||
[[nodiscard]] bool anySet() const {
|
||||
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight;
|
||||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width;
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||
width = 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,6 +118,7 @@ struct CssStyle {
|
||||
CssLength paddingBottom; // Padding after
|
||||
CssLength paddingLeft; // Padding left
|
||||
CssLength paddingRight; // Padding right
|
||||
CssLength width; // Element width (used for table columns/cells)
|
||||
|
||||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||
|
||||
@@ -173,6 +177,10 @@ struct CssStyle {
|
||||
paddingRight = base.paddingRight;
|
||||
defined.paddingRight = 1;
|
||||
}
|
||||
if (base.hasWidth()) {
|
||||
width = base.width;
|
||||
defined.width = 1;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
||||
@@ -188,6 +196,7 @@ struct CssStyle {
|
||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||||
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||||
[[nodiscard]] bool hasWidth() const { return defined.width; }
|
||||
|
||||
void reset() {
|
||||
textAlign = CssTextAlign::Left;
|
||||
@@ -197,6 +206,7 @@ struct CssStyle {
|
||||
textIndent = CssLength{};
|
||||
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||
width = CssLength{};
|
||||
defined.clearAll();
|
||||
}
|
||||
};
|
||||
|
||||
76
lib/Epub/Epub/htmlEntities.cpp
Normal file
76
lib/Epub/Epub/htmlEntities.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
// from
|
||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||
|
||||
#include "htmlEntities.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
struct EntityPair {
|
||||
const char* key;
|
||||
const char* value;
|
||||
};
|
||||
|
||||
static const EntityPair ENTITY_LOOKUP[] = {
|
||||
{""", "\""}, {"⁄", "⁄"}, {"&", "&"}, {"<", "<"}, {">", ">"},
|
||||
{"À", "À"}, {"Á", "Á"}, {"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"},
|
||||
{"Å", "Å"}, {"Æ", "Æ"}, {"Ç", "Ç"}, {"È", "È"}, {"É", "É"},
|
||||
{"Ê", "Ê"}, {"Ë", "Ë"}, {"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"},
|
||||
{"Ï", "Ï"}, {"Ð", "Ð"}, {"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"},
|
||||
{"Ô", "Ô"}, {"Õ", "Õ"}, {"Ö", "Ö"}, {"Ø", "Ø"}, {"Ù", "Ù"},
|
||||
{"Ú", "Ú"}, {"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"},
|
||||
{"ß", "ß"}, {"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"},
|
||||
{"ä", "ä"}, {"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"},
|
||||
{"é", "é"}, {"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"},
|
||||
{"î", "î"}, {"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"},
|
||||
{"ó", "ó"}, {"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"ø", "ø"},
|
||||
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"},
|
||||
{"þ", "þ"}, {"ÿ", "ÿ"}, {" ", "\xC2\xA0"}, {"¡", "¡"}, {"¢", "¢"},
|
||||
{"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, {"§", "§"},
|
||||
{"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, {"¬", "¬"},
|
||||
{"­", ""}, {"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"},
|
||||
{"²", "²"}, {"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"},
|
||||
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"},
|
||||
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"×", "×"}, {"÷", "÷"},
|
||||
{"∀", "∀"}, {"∂", "∂"}, {"∃", "∃"}, {"∅", "∅"}, {"∇", "∇"},
|
||||
{"∈", "∈"}, {"∉", "∉"}, {"∋", "∋"}, {"∏", "∏"}, {"∑", "∑"},
|
||||
{"−", "−"}, {"∗", "∗"}, {"√", "√"}, {"∝", "∝"}, {"∞", "∞"},
|
||||
{"∠", "∠"}, {"∧", "∧"}, {"∨", "∨"}, {"∩", "∩"}, {"∪", "∪"},
|
||||
{"∫", "∫"}, {"∴", "∴"}, {"∼", "∼"}, {"≅", "≅"}, {"≈", "≈"},
|
||||
{"≠", "≠"}, {"≡", "≡"}, {"≤", "≤"}, {"≥", "≥"}, {"⊂", "⊂"},
|
||||
{"⊃", "⊃"}, {"⊄", "⊄"}, {"⊆", "⊆"}, {"⊇", "⊇"}, {"⊕", "⊕"},
|
||||
{"⊗", "⊗"}, {"⊥", "⊥"}, {"⋅", "⋅"}, {"Α", "Α"}, {"Β", "Β"},
|
||||
{"Γ", "Γ"}, {"Δ", "Δ"}, {"Ε", "Ε"}, {"Ζ", "Ζ"}, {"Η", "Η"},
|
||||
{"Θ", "Θ"}, {"Ι", "Ι"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"},
|
||||
{"Ν", "Ν"}, {"Ξ", "Ξ"}, {"Ο", "Ο"}, {"Π", "Π"}, {"Ρ", "Ρ"},
|
||||
{"Σ", "Σ"}, {"Τ", "Τ"}, {"Υ", "Υ"}, {"Φ", "Φ"}, {"Χ", "Χ"},
|
||||
{"Ψ", "Ψ"}, {"Ω", "Ω"}, {"α", "α"}, {"β", "β"}, {"γ", "γ"},
|
||||
{"δ", "δ"}, {"ε", "ε"}, {"ζ", "ζ"}, {"η", "η"}, {"θ", "θ"},
|
||||
{"ι", "ι"}, {"κ", "κ"}, {"λ", "λ"}, {"μ", "μ"}, {"ν", "ν"},
|
||||
{"ξ", "ξ"}, {"ο", "ο"}, {"π", "π"}, {"ρ", "ρ"}, {"ς", "ς"},
|
||||
{"σ", "σ"}, {"τ", "τ"}, {"υ", "υ"}, {"φ", "φ"}, {"χ", "χ"},
|
||||
{"ψ", "ψ"}, {"ω", "ω"}, {"ϑ", "ϑ"}, {"ϒ", "ϒ"}, {"ϖ", "ϖ"},
|
||||
{"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, {"š", "š"}, {"Ÿ", "Ÿ"},
|
||||
{"ƒ", "ƒ"}, {"ˆ", "ˆ"}, {"˜", "˜"}, {" ", " "}, {" ", " "},
|
||||
{" ", " "}, {"‌", ""}, {"‍", ""}, {"‎", ""}, {"‏", ""},
|
||||
{"–", "–"}, {"—", "—"}, {"‘", "‘"}, {"’", "’"}, {"‚", "‚"},
|
||||
{"“", "“"}, {"”", "”"}, {"„", "„"}, {"†", "†"}, {"‡", "‡"},
|
||||
{"•", "•"}, {"…", "…"}, {"‰", "‰"}, {"′", "′"}, {"″", "″"},
|
||||
{"‹", "‹"}, {"›", "›"}, {"‾", "‾"}, {"€", "€"}, {"™", "™"},
|
||||
{"←", "←"}, {"↑", "↑"}, {"→", "→"}, {"↓", "↓"}, {"↔", "↔"},
|
||||
{"↵", "↵"}, {"⌈", "⌈"}, {"⌉", "⌉"}, {"⌊", "⌊"}, {"⌋", "⌋"},
|
||||
{"◊", "◊"}, {"♠", "♠"}, {"♣", "♣"}, {"♥", "♥"}, {"♦", "♦"}};
|
||||
|
||||
static const size_t ENTITY_LOOKUP_COUNT = sizeof(ENTITY_LOOKUP) / sizeof(ENTITY_LOOKUP[0]);
|
||||
|
||||
// Lookup a single HTML entity and return its UTF-8 value
|
||||
const char* lookupHtmlEntity(const char* entity, int len) {
|
||||
for (size_t i = 0; i < ENTITY_LOOKUP_COUNT; i++) {
|
||||
const char* key = ENTITY_LOOKUP[i].key;
|
||||
const size_t keyLen = strlen(key);
|
||||
if (static_cast<size_t>(len) == keyLen && memcmp(entity, key, keyLen) == 0) {
|
||||
return ENTITY_LOOKUP[i].value;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr; // Entity not found
|
||||
}
|
||||
9
lib/Epub/Epub/htmlEntities.h
Normal file
9
lib/Epub/Epub/htmlEntities.h
Normal file
@@ -0,0 +1,9 @@
|
||||
// from
|
||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
// Lookup a single HTML entity (including & and ;) and return its UTF-8 value
|
||||
// Returns nullptr if entity is not found
|
||||
const char* lookupHtmlEntity(const char* entity, int len);
|
||||
@@ -1,45 +1,84 @@
|
||||
#include "LanguageRegistry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "HyphenationCommon.h"
|
||||
#ifndef OMIT_HYPH_DE
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_EN
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_ES
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_FR
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_IT
|
||||
#include "generated/hyph-it.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_RU
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#ifndef OMIT_HYPH_EN
|
||||
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||
LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_FR
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_DE
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_RU
|
||||
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_ES
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_IT
|
||||
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 5>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
{"german", "de", &germanHyphenator},
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
{"spanish", "es", &spanishHyphenator}}};
|
||||
return kEntries;
|
||||
const LanguageEntryView entries() {
|
||||
static const std::vector<LanguageEntry> kEntries = {
|
||||
#ifndef OMIT_HYPH_EN
|
||||
{"english", "en", &englishHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_FR
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_DE
|
||||
{"german", "de", &germanHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_RU
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_ES
|
||||
{"spanish", "es", &spanishHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_IT
|
||||
{"italian", "it", &italianHyphenator},
|
||||
#endif
|
||||
};
|
||||
static const LanguageEntryView view{kEntries.data(), kEntries.size()};
|
||||
return view;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) {
|
||||
const auto& allEntries = entries();
|
||||
const auto allEntries = entries();
|
||||
const auto it = std::find_if(allEntries.begin(), allEntries.end(),
|
||||
[&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; });
|
||||
return (it != allEntries.end()) ? it->hyphenator : nullptr;
|
||||
}
|
||||
|
||||
LanguageEntryView getLanguageEntries() {
|
||||
const auto& allEntries = entries();
|
||||
return LanguageEntryView{allEntries.data(), allEntries.size()};
|
||||
return entries();
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
|
||||
namespace {
|
||||
|
||||
using EmbeddedAutomaton = SerializedHyphenationPatterns;
|
||||
|
||||
struct AugmentedWord {
|
||||
std::vector<uint8_t> bytes;
|
||||
std::vector<size_t> charByteOffsets;
|
||||
@@ -141,59 +143,10 @@ struct AutomatonState {
|
||||
bool valid() const { return data != nullptr; }
|
||||
};
|
||||
|
||||
// Lightweight descriptor for the entire embedded automaton.
|
||||
// The blob format is:
|
||||
// [0..3] - big-endian root offset
|
||||
// [4....] - node heap containing variable-sized headers + transition data
|
||||
struct EmbeddedAutomaton {
|
||||
const uint8_t* data = nullptr;
|
||||
size_t size = 0;
|
||||
uint32_t rootOffset = 0;
|
||||
|
||||
bool valid() const { return data != nullptr && size >= 4 && rootOffset < size; }
|
||||
};
|
||||
|
||||
// Decode the serialized automaton header and root offset.
|
||||
EmbeddedAutomaton parseAutomaton(const SerializedHyphenationPatterns& patterns) {
|
||||
EmbeddedAutomaton automaton;
|
||||
if (!patterns.data || patterns.size < 4) {
|
||||
return automaton;
|
||||
}
|
||||
|
||||
automaton.data = patterns.data;
|
||||
automaton.size = patterns.size;
|
||||
automaton.rootOffset = (static_cast<uint32_t>(patterns.data[0]) << 24) |
|
||||
(static_cast<uint32_t>(patterns.data[1]) << 16) |
|
||||
(static_cast<uint32_t>(patterns.data[2]) << 8) | static_cast<uint32_t>(patterns.data[3]);
|
||||
if (automaton.rootOffset >= automaton.size) {
|
||||
automaton.data = nullptr;
|
||||
automaton.size = 0;
|
||||
}
|
||||
return automaton;
|
||||
}
|
||||
|
||||
// Cache parsed automata per blob pointer to avoid reparsing.
|
||||
const EmbeddedAutomaton& getAutomaton(const SerializedHyphenationPatterns& patterns) {
|
||||
struct CacheEntry {
|
||||
const SerializedHyphenationPatterns* key;
|
||||
EmbeddedAutomaton automaton;
|
||||
};
|
||||
static std::vector<CacheEntry> cache;
|
||||
|
||||
for (const auto& entry : cache) {
|
||||
if (entry.key == &patterns) {
|
||||
return entry.automaton;
|
||||
}
|
||||
}
|
||||
|
||||
cache.push_back({&patterns, parseAutomaton(patterns)});
|
||||
return cache.back().automaton;
|
||||
}
|
||||
|
||||
// Interpret the node located at `addr`, returning transition metadata.
|
||||
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
||||
AutomatonState state;
|
||||
if (!automaton.valid() || addr >= automaton.size) {
|
||||
if (addr >= automaton.size) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -234,7 +187,7 @@ AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
||||
if (offset + levelsLen > automaton.size) {
|
||||
return AutomatonState{};
|
||||
}
|
||||
levelsPtr = automaton.data + offset;
|
||||
levelsPtr = automaton.data + offset - 4u;
|
||||
}
|
||||
|
||||
if (pos + childCount > remaining) {
|
||||
@@ -344,10 +297,7 @@ std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
|
||||
return {};
|
||||
}
|
||||
|
||||
const EmbeddedAutomaton& automaton = getAutomaton(patterns);
|
||||
if (!automaton.valid()) {
|
||||
return {};
|
||||
}
|
||||
const EmbeddedAutomaton& automaton = patterns;
|
||||
|
||||
const AutomatonState root = decodeState(automaton, automaton.rootOffset);
|
||||
if (!root.valid()) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
|
||||
struct SerializedHyphenationPatterns {
|
||||
size_t rootOffset;
|
||||
const std::uint8_t* data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,377 +7,447 @@
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t fr_trie_data[] = {
|
||||
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C,
|
||||
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F,
|
||||
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C,
|
||||
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D,
|
||||
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E,
|
||||
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16,
|
||||
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16,
|
||||
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C,
|
||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD,
|
||||
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF,
|
||||
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72,
|
||||
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD,
|
||||
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
||||
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61,
|
||||
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74,
|
||||
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75,
|
||||
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF,
|
||||
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD,
|
||||
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41,
|
||||
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62,
|
||||
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06,
|
||||
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21,
|
||||
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
||||
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF,
|
||||
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65,
|
||||
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02,
|
||||
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5,
|
||||
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8,
|
||||
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64,
|
||||
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67,
|
||||
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01,
|
||||
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1,
|
||||
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE,
|
||||
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75,
|
||||
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC,
|
||||
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04,
|
||||
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41,
|
||||
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04,
|
||||
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61,
|
||||
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0,
|
||||
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04,
|
||||
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3,
|
||||
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73,
|
||||
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
||||
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF,
|
||||
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41,
|
||||
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72,
|
||||
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05,
|
||||
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E,
|
||||
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44,
|
||||
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86,
|
||||
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D,
|
||||
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41,
|
||||
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A,
|
||||
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F,
|
||||
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75,
|
||||
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61,
|
||||
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21,
|
||||
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
||||
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21,
|
||||
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6,
|
||||
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F,
|
||||
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68,
|
||||
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF,
|
||||
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74,
|
||||
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01,
|
||||
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA,
|
||||
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73,
|
||||
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E,
|
||||
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21,
|
||||
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21,
|
||||
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75,
|
||||
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61,
|
||||
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA,
|
||||
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
||||
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21,
|
||||
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF,
|
||||
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43,
|
||||
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21,
|
||||
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB,
|
||||
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE,
|
||||
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF,
|
||||
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41,
|
||||
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25,
|
||||
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC,
|
||||
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70,
|
||||
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC,
|
||||
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
||||
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41,
|
||||
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08,
|
||||
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9,
|
||||
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF,
|
||||
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75,
|
||||
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD,
|
||||
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18,
|
||||
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2,
|
||||
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65,
|
||||
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF,
|
||||
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63,
|
||||
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69,
|
||||
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21,
|
||||
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21,
|
||||
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7,
|
||||
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
||||
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2,
|
||||
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47,
|
||||
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7,
|
||||
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9,
|
||||
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63,
|
||||
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8,
|
||||
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75,
|
||||
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6,
|
||||
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6,
|
||||
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61,
|
||||
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6,
|
||||
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8,
|
||||
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61,
|
||||
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8,
|
||||
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
||||
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0,
|
||||
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
|
||||
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21,
|
||||
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63,
|
||||
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1,
|
||||
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70,
|
||||
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21,
|
||||
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21,
|
||||
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03,
|
||||
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76,
|
||||
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2,
|
||||
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
||||
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21,
|
||||
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4,
|
||||
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2,
|
||||
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE,
|
||||
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65,
|
||||
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF,
|
||||
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF,
|
||||
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
||||
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92,
|
||||
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF,
|
||||
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F,
|
||||
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD,
|
||||
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41,
|
||||
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD,
|
||||
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA,
|
||||
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21,
|
||||
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2,
|
||||
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59,
|
||||
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE,
|
||||
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00,
|
||||
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
||||
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0,
|
||||
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75,
|
||||
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7,
|
||||
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69,
|
||||
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C,
|
||||
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2,
|
||||
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC,
|
||||
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD,
|
||||
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3,
|
||||
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41,
|
||||
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74,
|
||||
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
||||
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9,
|
||||
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21,
|
||||
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41,
|
||||
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73,
|
||||
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34,
|
||||
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E,
|
||||
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A,
|
||||
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21,
|
||||
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9,
|
||||
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA,
|
||||
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42,
|
||||
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB,
|
||||
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1,
|
||||
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41,
|
||||
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
||||
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63,
|
||||
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD,
|
||||
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3,
|
||||
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65,
|
||||
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62,
|
||||
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD,
|
||||
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01,
|
||||
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9,
|
||||
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1,
|
||||
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21,
|
||||
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75,
|
||||
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
||||
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0,
|
||||
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47,
|
||||
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00,
|
||||
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0,
|
||||
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69,
|
||||
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA,
|
||||
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22,
|
||||
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22,
|
||||
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73,
|
||||
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF,
|
||||
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1,
|
||||
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F,
|
||||
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C,
|
||||
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66,
|
||||
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
||||
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63,
|
||||
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD,
|
||||
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F,
|
||||
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21,
|
||||
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72,
|
||||
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61,
|
||||
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF,
|
||||
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68,
|
||||
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61,
|
||||
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61,
|
||||
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB,
|
||||
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
||||
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1,
|
||||
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
||||
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C,
|
||||
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F,
|
||||
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F,
|
||||
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF,
|
||||
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6,
|
||||
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45,
|
||||
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21,
|
||||
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79,
|
||||
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D,
|
||||
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D,
|
||||
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
||||
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8,
|
||||
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7,
|
||||
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F,
|
||||
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
||||
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72,
|
||||
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB,
|
||||
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0,
|
||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5,
|
||||
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA,
|
||||
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D,
|
||||
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1,
|
||||
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D,
|
||||
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5,
|
||||
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2,
|
||||
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4,
|
||||
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87,
|
||||
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2,
|
||||
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C,
|
||||
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
||||
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69,
|
||||
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
||||
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF,
|
||||
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02,
|
||||
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72,
|
||||
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75,
|
||||
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65,
|
||||
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76,
|
||||
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9,
|
||||
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66,
|
||||
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E,
|
||||
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64,
|
||||
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21,
|
||||
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
||||
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21,
|
||||
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65,
|
||||
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF,
|
||||
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11,
|
||||
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21,
|
||||
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6,
|
||||
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF,
|
||||
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82,
|
||||
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F,
|
||||
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74,
|
||||
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74,
|
||||
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73,
|
||||
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3,
|
||||
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D,
|
||||
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1,
|
||||
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2,
|
||||
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65,
|
||||
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD,
|
||||
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1,
|
||||
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76,
|
||||
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21,
|
||||
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42,
|
||||
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2,
|
||||
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74,
|
||||
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42,
|
||||
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41,
|
||||
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2,
|
||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0,
|
||||
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
||||
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0,
|
||||
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8,
|
||||
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0,
|
||||
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79,
|
||||
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80,
|
||||
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF,
|
||||
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0,
|
||||
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1,
|
||||
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E,
|
||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F,
|
||||
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF,
|
||||
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38,
|
||||
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2,
|
||||
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF,
|
||||
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
||||
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0,
|
||||
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98,
|
||||
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE,
|
||||
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
||||
0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x2B,
|
||||
0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17,
|
||||
0x04, 0x1F, 0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36,
|
||||
0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C,
|
||||
0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B,
|
||||
0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x48, 0x2C, 0x0B, 0x29, 0x16,
|
||||
0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x3E, 0x0D,
|
||||
0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B,
|
||||
0x16, 0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0,
|
||||
0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61,
|
||||
0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA,
|
||||
0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD,
|
||||
0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x5E, 0x21, 0x74, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x73, 0x72, 0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF,
|
||||
0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0,
|
||||
0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0x6E, 0x75, 0xF2, 0xFD, 0x21,
|
||||
0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0xFF, 0x06,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2,
|
||||
0x02, 0x52, 0x6E, 0x75, 0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD,
|
||||
0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70,
|
||||
0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x72, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96,
|
||||
0xFF, 0x96, 0x41, 0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52,
|
||||
0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05,
|
||||
0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75,
|
||||
0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0xC2, 0x02,
|
||||
0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74,
|
||||
0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF,
|
||||
0xF4, 0xFF, 0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7,
|
||||
0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xFD, 0xA0, 0x00, 0x61,
|
||||
0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0xFE, 0xC8,
|
||||
0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43,
|
||||
0x64, 0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E,
|
||||
0x70, 0x73, 0x72, 0x67, 0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D,
|
||||
0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2,
|
||||
0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x47, 0xFE, 0x47,
|
||||
0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43,
|
||||
0x63, 0x74, 0x75, 0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F,
|
||||
0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0,
|
||||
0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||
0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0x6C,
|
||||
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD,
|
||||
0xA0, 0x04, 0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD,
|
||||
0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02,
|
||||
0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x31, 0x21,
|
||||
0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41,
|
||||
0x61, 0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41,
|
||||
0x6F, 0xFE, 0x7B, 0xA0, 0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63,
|
||||
0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3,
|
||||
0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x74,
|
||||
0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
||||
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E,
|
||||
0x72, 0x73, 0xFF, 0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9,
|
||||
0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C,
|
||||
0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1,
|
||||
0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x23, 0x21, 0x6E, 0xFD,
|
||||
0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0xFF,
|
||||
0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF,
|
||||
0xFD, 0x44, 0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9,
|
||||
0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64,
|
||||
0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C,
|
||||
0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x68, 0xFC, 0x92, 0x23, 0x61,
|
||||
0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0xFC, 0x5A,
|
||||
0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79,
|
||||
0x6F, 0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69,
|
||||
0x6F, 0x73, 0x74, 0x75, 0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0,
|
||||
0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE,
|
||||
0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63,
|
||||
0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x6E, 0xFB, 0x41,
|
||||
0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
||||
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21,
|
||||
0xC3, 0xFC, 0x21, 0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3,
|
||||
0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64,
|
||||
0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD,
|
||||
0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xFE, 0xCA, 0x21, 0x6F,
|
||||
0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0x44,
|
||||
0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD,
|
||||
0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA,
|
||||
0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xFA, 0xA4, 0xFA, 0xA4, 0xFF,
|
||||
0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0xFD, 0x44,
|
||||
0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41,
|
||||
0xA9, 0xFC, 0x27, 0x21, 0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21,
|
||||
0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2,
|
||||
0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73,
|
||||
0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0x21, 0xA9,
|
||||
0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
||||
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4,
|
||||
0xFC, 0xBD, 0x21, 0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93,
|
||||
0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9,
|
||||
0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD,
|
||||
0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xA0, 0x01, 0xC1, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x81,
|
||||
0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41,
|
||||
0x73, 0xFE, 0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD,
|
||||
0x43, 0x6F, 0x73, 0x75, 0xFF, 0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD,
|
||||
0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0x61, 0xFE, 0xA9, 0x21, 0x69,
|
||||
0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0x21, 0x74,
|
||||
0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD,
|
||||
0x25, 0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB,
|
||||
0xFD, 0x21, 0x61, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD,
|
||||
0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68,
|
||||
0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA,
|
||||
0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xFB,
|
||||
0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
||||
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7,
|
||||
0xFF, 0xFD, 0x41, 0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C,
|
||||
0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80,
|
||||
0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9,
|
||||
0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0xD7, 0xFF, 0xE4, 0xFD,
|
||||
0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xB9,
|
||||
0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21,
|
||||
0xA9, 0xFD, 0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9,
|
||||
0x42, 0x66, 0x78, 0xFB, 0x18, 0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1,
|
||||
0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41,
|
||||
0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x63, 0x68, 0x75, 0x6F, 0xFF,
|
||||
0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0xFD, 0xC3,
|
||||
0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43,
|
||||
0x63, 0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F,
|
||||
0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41,
|
||||
0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0xF8, 0xFF, 0xF9,
|
||||
0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
||||
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41,
|
||||
0x69, 0xF7, 0xD2, 0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73,
|
||||
0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF,
|
||||
0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70,
|
||||
0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x41, 0x75, 0xF8, 0x30,
|
||||
0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0xF8,
|
||||
0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41,
|
||||
0x73, 0xF8, 0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41,
|
||||
0x69, 0xF8, 0x73, 0x21, 0x75, 0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA,
|
||||
0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72,
|
||||
0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0xBF, 0xF6, 0xBF, 0x42, 0x63,
|
||||
0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x74,
|
||||
0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2,
|
||||
0x61, 0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF,
|
||||
0xF9, 0xF6, 0x99, 0xF6, 0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF,
|
||||
0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF,
|
||||
0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xC3, 0x62, 0x63, 0x64, 0x65, 0x69,
|
||||
0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0xB1, 0xF8, 0xE6,
|
||||
0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
||||
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85,
|
||||
0xF8, 0x85, 0xA0, 0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73,
|
||||
0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21,
|
||||
0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F,
|
||||
0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x61,
|
||||
0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
||||
0xFD, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0xE1, 0xFD, 0x41, 0x74, 0xFE,
|
||||
0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xF6, 0xFD,
|
||||
0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41,
|
||||
0x2E, 0xFF, 0x33, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63,
|
||||
0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22,
|
||||
0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64,
|
||||
0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6,
|
||||
0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xC9, 0xFF, 0xD4,
|
||||
0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02,
|
||||
0x41, 0x21, 0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1,
|
||||
0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2,
|
||||
0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74,
|
||||
0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x00, 0xE2, 0x65, 0x75, 0xFF,
|
||||
0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0x62, 0xFF,
|
||||
0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43,
|
||||
0x65, 0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65,
|
||||
0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0xE7, 0xFF, 0xF6,
|
||||
0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
||||
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC,
|
||||
0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48,
|
||||
0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFF, 0xA8, 0xFF, 0xBF,
|
||||
0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x8D,
|
||||
0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75,
|
||||
0xFD, 0x41, 0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70,
|
||||
0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0xFF, 0xFD, 0x42, 0x2E, 0x73,
|
||||
0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x74, 0xFD,
|
||||
0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F,
|
||||
0xE2, 0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21,
|
||||
0x6E, 0xFC, 0x21, 0x65, 0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE,
|
||||
0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41,
|
||||
0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xE2, 0x2E, 0x62,
|
||||
0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
||||
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D,
|
||||
0xFC, 0xEE, 0xA0, 0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5,
|
||||
0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72,
|
||||
0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD,
|
||||
0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0x72, 0xFC, 0x41, 0x69,
|
||||
0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||
0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4,
|
||||
0x21, 0x69, 0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC,
|
||||
0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7,
|
||||
0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF,
|
||||
0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0x88, 0xA1,
|
||||
0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69,
|
||||
0xFC, 0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE,
|
||||
0x8A, 0xFD, 0x27, 0xFD, 0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF,
|
||||
0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1,
|
||||
0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC,
|
||||
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0xF2, 0x5A, 0xA1,
|
||||
0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
||||
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2,
|
||||
0xF5, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0,
|
||||
0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1,
|
||||
0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFB, 0x41, 0xFF, 0xFB,
|
||||
0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0xFC,
|
||||
0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73,
|
||||
0xFB, 0x34, 0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69,
|
||||
0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08,
|
||||
0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54,
|
||||
0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xF3,
|
||||
0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0xFF, 0xFC,
|
||||
0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5,
|
||||
0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF,
|
||||
0xFC, 0xFB, 0x62, 0x42, 0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E,
|
||||
0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C,
|
||||
0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD,
|
||||
0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x68, 0xF8, 0xC0,
|
||||
0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
||||
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A,
|
||||
0xC3, 0x69, 0x63, 0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39,
|
||||
0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03,
|
||||
0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A,
|
||||
0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x76,
|
||||
0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0x73,
|
||||
0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D,
|
||||
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2,
|
||||
0x69, 0x75, 0xC3, 0x6F, 0x65, 0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41,
|
||||
0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9,
|
||||
0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFC, 0x3E, 0xFC, 0x3E, 0x41,
|
||||
0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0xFF, 0xFC,
|
||||
0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C,
|
||||
0xEB, 0xFD, 0x42, 0xA9, 0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||
0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D,
|
||||
0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0x65, 0xF9, 0x7E,
|
||||
0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
||||
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21,
|
||||
0x75, 0xFC, 0xA0, 0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00,
|
||||
0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF,
|
||||
0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB,
|
||||
0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x07, 0x62, 0x21, 0xA9,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0xFA,
|
||||
0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35,
|
||||
0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6,
|
||||
0x41, 0x75, 0xF8, 0xC2, 0x22, 0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC,
|
||||
0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D,
|
||||
0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x74, 0x79, 0xFE, 0xAE, 0xFE,
|
||||
0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xC2, 0xFF,
|
||||
0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64,
|
||||
0xF1, 0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC,
|
||||
0x41, 0x6C, 0xF1, 0x8F, 0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42,
|
||||
0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0xB5, 0xBC, 0xCE,
|
||||
0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
||||
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61,
|
||||
0xFC, 0x22, 0x63, 0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1,
|
||||
0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70,
|
||||
0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x72, 0xF7, 0x21, 0x68,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFA,
|
||||
0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD,
|
||||
0x41, 0x61, 0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72,
|
||||
0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC,
|
||||
0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69,
|
||||
0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||
0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xFF, 0xA5,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21,
|
||||
0x61, 0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA,
|
||||
0xFF, 0xDF, 0xFF, 0xEB, 0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61,
|
||||
0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63,
|
||||
0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6,
|
||||
0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0x41, 0x70, 0xED,
|
||||
0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
||||
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70,
|
||||
0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21,
|
||||
0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79,
|
||||
0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xFD,
|
||||
0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72,
|
||||
0xF6, 0xA6, 0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74,
|
||||
0xF0, 0x03, 0xFF, 0xFC, 0x45, 0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD,
|
||||
0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61,
|
||||
0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0xFB, 0x9D, 0x21, 0x68, 0xFC,
|
||||
0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xFB, 0xEE,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41,
|
||||
0x6D, 0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1,
|
||||
0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2,
|
||||
0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21,
|
||||
0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7,
|
||||
0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xFF, 0xF3, 0xF7,
|
||||
0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
||||
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00,
|
||||
0xE1, 0x6D, 0x72, 0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65,
|
||||
0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3,
|
||||
0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5,
|
||||
0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0x41,
|
||||
0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x2E,
|
||||
0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20,
|
||||
0xFF, 0x4D, 0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC,
|
||||
0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75,
|
||||
0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0x32, 0xFF, 0xFD, 0xC2, 0x00,
|
||||
0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0x65, 0x69,
|
||||
0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD,
|
||||
0xC4, 0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD,
|
||||
0xC4, 0xF4, 0xD1, 0x45, 0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4,
|
||||
0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2,
|
||||
0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0x61, 0xC3, 0x65,
|
||||
0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
||||
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4,
|
||||
0x79, 0x41, 0x69, 0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5,
|
||||
0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5,
|
||||
0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21,
|
||||
0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0x6F, 0x73, 0xF5, 0x12,
|
||||
0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0x70,
|
||||
0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00,
|
||||
0xE2, 0x75, 0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD,
|
||||
0x41, 0x6D, 0xF4, 0x02, 0x21, 0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22,
|
||||
0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0,
|
||||
0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0xFF, 0xFC, 0xF3, 0xDA, 0x41,
|
||||
0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xFB, 0x17,
|
||||
0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6,
|
||||
0x66, 0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29,
|
||||
0xC6, 0x00, 0xE2, 0x2E, 0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF,
|
||||
0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21,
|
||||
0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA9,
|
||||
0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
||||
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65,
|
||||
0xF3, 0x44, 0x21, 0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9,
|
||||
0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5,
|
||||
0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F,
|
||||
0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x41, 0x69, 0xF3, 0x97,
|
||||
0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0x6E,
|
||||
0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41,
|
||||
0x6F, 0xF6, 0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62,
|
||||
0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21,
|
||||
0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF7, 0x81, 0x21, 0x72, 0xFC,
|
||||
0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF7, 0x6F,
|
||||
0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC,
|
||||
0xDC, 0xFB, 0x21, 0x74, 0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D,
|
||||
0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D,
|
||||
0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E,
|
||||
0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x72, 0xFF, 0xFB,
|
||||
0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1,
|
||||
0x00, 0xE1, 0x6D, 0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB,
|
||||
0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06,
|
||||
0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD,
|
||||
0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
||||
0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x22,
|
||||
0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75,
|
||||
0x79, 0xF1, 0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41,
|
||||
0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC,
|
||||
0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73,
|
||||
0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x6E, 0x73, 0xE9, 0xBA, 0xE9,
|
||||
0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x00, 0xE1,
|
||||
0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41,
|
||||
0x74, 0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF,
|
||||
0xFD, 0xF2, 0x88, 0x42, 0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73,
|
||||
0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0x41, 0x6D,
|
||||
0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
||||
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB,
|
||||
0xF2, 0x50, 0xF0, 0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42,
|
||||
0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48,
|
||||
0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6,
|
||||
0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0x21, 0x74, 0xFC, 0x21,
|
||||
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0x6D,
|
||||
0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80,
|
||||
0xF0, 0x80, 0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1,
|
||||
0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0,
|
||||
0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0,
|
||||
0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0x41, 0x69, 0xFA,
|
||||
0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0xFD, 0x22,
|
||||
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69,
|
||||
0x6F, 0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0,
|
||||
0x0B, 0xF4, 0x0D, 0xFF, 0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC,
|
||||
0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61,
|
||||
0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||
0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB,
|
||||
0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
||||
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0,
|
||||
0xEF, 0x48, 0xF0, 0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F,
|
||||
0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4,
|
||||
0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65,
|
||||
0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns fr_patterns = {
|
||||
0x1AF0u,
|
||||
fr_trie_data,
|
||||
sizeof(fr_trie_data),
|
||||
};
|
||||
|
||||
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||
0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x0D, 0x16, 0x0B, 0x34,
|
||||
0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x01, 0x02, 0x16, 0x02,
|
||||
0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0B, 0xA0, 0x00, 0x42,
|
||||
0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x6D, 0xFD, 0x21, 0x69,
|
||||
0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x6D,
|
||||
0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x61, 0x69, 0x6F, 0xDF,
|
||||
0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x75, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x00,
|
||||
0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0xA0, 0x01, 0x52, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x71, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x72,
|
||||
0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0x6C, 0x72, 0xFD, 0xFD,
|
||||
0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x11, 0x25, 0x61, 0x68,
|
||||
0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x72, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xA2, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x72, 0xFF, 0xFC, 0xFF,
|
||||
0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x72,
|
||||
0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0x21,
|
||||
0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0xFD, 0x41, 0x6E, 0xFF,
|
||||
0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x61, 0x65,
|
||||
0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||
0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x12, 0xFF, 0x20, 0xFF,
|
||||
0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0xC2, 0xFF, 0xE6, 0xFF,
|
||||
0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xA0, 0x00, 0xD1, 0x24,
|
||||
0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0xF1, 0xA0, 0x01,
|
||||
0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xFD, 0x21, 0x75, 0xDF,
|
||||
0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0x02, 0x01, 0x62, 0x63,
|
||||
0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0xE3, 0xE3, 0xE3, 0xE3,
|
||||
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0x27, 0xC4, 0xC7, 0xC6,
|
||||
0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xFB, 0xFF, 0xBF,
|
||||
0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x6E, 0x71, 0x73,
|
||||
0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xEB, 0xFF,
|
||||
0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0x67, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||
0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0x72, 0x73, 0x74, 0x2E,
|
||||
0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x4A, 0xFF,
|
||||
0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0xFD, 0xD1, 0x02, 0x01,
|
||||
0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E,
|
||||
0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x33, 0xFF, 0x21, 0xFF,
|
||||
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
|
||||
0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x68, 0x69, 0x6C, 0x6D,
|
||||
0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0xFC, 0xFE, 0xF9, 0xFE,
|
||||
0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0x02, 0x01, 0x2E, 0x27,
|
||||
0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0x6D, 0x72, 0x73, 0x74,
|
||||
0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
|
||||
0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0x2E, 0x27, 0xFE, 0x93,
|
||||
0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||
0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||
0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||
0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0x21, 0x72, 0xF8, 0x21,
|
||||
0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x69, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||
0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||
0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x82,
|
||||
0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xFD, 0xCB, 0x02, 0x01,
|
||||
0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0xB1, 0xFD, 0xC3, 0xFD,
|
||||
0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD,
|
||||
0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0x8D, 0xA0, 0x02, 0x53,
|
||||
0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x68, 0x67, 0x6B, 0x6C,
|
||||
0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x27, 0xFD, 0x79, 0xFD,
|
||||
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x37, 0xFD, 0x37, 0xFD,
|
||||
0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x8F, 0x4B, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
|
||||
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xA0,
|
||||
0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x70, 0x74, 0x7A, 0x2E,
|
||||
0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0xF8, 0xFF, 0xFB, 0xC1,
|
||||
0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x63, 0xFC, 0xC1, 0x01,
|
||||
0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0x06, 0xD2, 0x02, 0x01,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A,
|
||||
0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xE2, 0xFC, 0xD3,
|
||||
0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1,
|
||||
0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0x76, 0x2E, 0x27, 0xFC,
|
||||
0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x72, 0xFB, 0xAF, 0xA0,
|
||||
0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0xFF, 0xF9, 0xFF, 0xFD,
|
||||
0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0x70, 0x74, 0x77, 0x2E,
|
||||
0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
|
||||
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0xCB, 0x02, 0x01, 0x62,
|
||||
0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
|
||||
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFD, 0x9F,
|
||||
0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0xC2, 0xFB, 0xF9, 0xFC,
|
||||
0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0xC4, 0xFC, 0xED, 0xFD,
|
||||
0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x5D, 0xFE, 0x81, 0xFE,
|
||||
0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xD5, 0xFF, 0xDC,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns it_patterns = {
|
||||
0x5C0u,
|
||||
it_trie_data,
|
||||
sizeof(it_trie_data),
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,24 @@
|
||||
#include "ChapterHtmlSlimParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "../../Epub.h"
|
||||
#include "../Page.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
#include "../converters/ImageToFramebufferDecoder.h"
|
||||
#include "../htmlEntities.h"
|
||||
|
||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
|
||||
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
||||
constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB
|
||||
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
@@ -31,8 +38,30 @@ constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
const char* SKIP_TAGS[] = {"head"};
|
||||
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
|
||||
|
||||
// Table tags that are transparent containers (just depth tracking, no special handling)
|
||||
const char* TABLE_TRANSPARENT_TAGS[] = {"thead", "tbody", "tfoot", "colgroup"};
|
||||
constexpr int NUM_TABLE_TRANSPARENT_TAGS = sizeof(TABLE_TRANSPARENT_TAGS) / sizeof(TABLE_TRANSPARENT_TAGS[0]);
|
||||
|
||||
// Table tags to skip entirely (their children produce no useful output)
|
||||
const char* TABLE_SKIP_TAGS[] = {"caption"};
|
||||
constexpr int NUM_TABLE_SKIP_TAGS = sizeof(TABLE_SKIP_TAGS) / sizeof(TABLE_SKIP_TAGS[0]);
|
||||
|
||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||
|
||||
// Parse an HTML width attribute value into a CssLength.
|
||||
// "200" -> 200px, "50%" -> 50 percent. Returns false if the value can't be parsed.
|
||||
static bool parseHtmlWidthAttr(const char* value, CssLength& out) {
|
||||
char* end = nullptr;
|
||||
const float num = strtof(value, &end);
|
||||
if (end == value || num < 0) return false;
|
||||
if (*end == '%') {
|
||||
out = CssLength(num, CssUnit::Percent);
|
||||
} else {
|
||||
out = CssLength(num, CssUnit::Pixels);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// given the start and end of a tag, check to see if it matches a known tag
|
||||
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
|
||||
for (int i = 0; i < possible_tag_count; i++) {
|
||||
@@ -90,13 +119,37 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
|
||||
// flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
|
||||
// Handle double-encoded entities (e.g. &nbsp; in source -> literal " " after
|
||||
// XML parsing). Common in Wikipedia and other generated EPUBs. Replace with a space so the text
|
||||
// renders cleanly. The space stays within the word, preserving non-breaking behavior.
|
||||
std::string flushedWord(partWordBuffer);
|
||||
size_t entityPos = 0;
|
||||
while ((entityPos = flushedWord.find(" ", entityPos)) != std::string::npos) {
|
||||
flushedWord.replace(entityPos, 6, " ");
|
||||
entityPos += 1;
|
||||
}
|
||||
|
||||
currentTextBlock->addWord(flushedWord, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
// When inside a table cell, don't lay out to the page -- insert a forced line break
|
||||
// within the cell's ParsedText so that block elements (p, div, br) create visual breaks.
|
||||
if (inTable) {
|
||||
if (partWordBufferIndex > 0) {
|
||||
flushPartWordBuffer();
|
||||
}
|
||||
if (currentTextBlock && !currentTextBlock->isEmpty()) {
|
||||
currentTextBlock->addLineBreak();
|
||||
}
|
||||
nextWordContinues = false;
|
||||
return;
|
||||
}
|
||||
|
||||
nextWordContinues = false; // New block = new paragraph, no continuation
|
||||
if (currentTextBlock) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
@@ -139,46 +192,304 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
centeredBlockStyle.textAlignDefined = true;
|
||||
centeredBlockStyle.alignment = CssTextAlign::Center;
|
||||
|
||||
// Special handling for tables - show placeholder text instead of dropping silently
|
||||
// --- Table handling ---
|
||||
if (strcmp(name, "table") == 0) {
|
||||
// Add placeholder text
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
if (self->inTable) {
|
||||
// Nested table: skip it entirely for v1
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush any pending content before the table
|
||||
if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) {
|
||||
self->makePages();
|
||||
}
|
||||
|
||||
self->inTable = true;
|
||||
self->tableData.reset(new TableData());
|
||||
|
||||
// Create a safe empty currentTextBlock so character data outside cells
|
||||
// (e.g. whitespace between tags) doesn't crash
|
||||
auto tableBlockStyle = BlockStyle();
|
||||
tableBlockStyle.alignment = CssTextAlign::Left;
|
||||
self->currentTextBlock.reset(new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, tableBlockStyle));
|
||||
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for an element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
std::string alt = "[Image]";
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "alt") == 0) {
|
||||
if (strlen(atts[i + 1]) > 0) {
|
||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Table structure tags (only when inside a table)
|
||||
if (self->inTable) {
|
||||
if (strcmp(name, "tr") == 0) {
|
||||
self->tableData->rows.push_back(TableRow());
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
// <col> — capture width hint for column sizing
|
||||
if (strcmp(name, "col") == 0) {
|
||||
CssLength widthHint;
|
||||
bool hasHint = false;
|
||||
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for an element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
// Parse HTML width attribute
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "width") == 0) {
|
||||
hasHint = parseHtmlWidthAttr(atts[i + 1], widthHint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
// CSS width (inline style) overrides HTML attribute
|
||||
if (self->cssParser) {
|
||||
std::string styleAttr;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "style") == 0) {
|
||||
styleAttr = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!styleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||
if (inlineStyle.hasWidth()) {
|
||||
widthHint = inlineStyle.width;
|
||||
hasHint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHint) {
|
||||
self->tableData->colWidthHints.push_back(widthHint);
|
||||
} else {
|
||||
// Push a zero-value placeholder to maintain index alignment
|
||||
self->tableData->colWidthHints.push_back(CssLength());
|
||||
}
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) {
|
||||
const bool isHeader = strcmp(name, "th") == 0;
|
||||
|
||||
// Parse colspan and width attributes
|
||||
int colspan = 1;
|
||||
CssLength cellWidthHint;
|
||||
bool hasCellWidthHint = false;
|
||||
std::string cellStyleAttr;
|
||||
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "colspan") == 0) {
|
||||
colspan = atoi(atts[i + 1]);
|
||||
if (colspan < 1) colspan = 1;
|
||||
} else if (strcmp(atts[i], "width") == 0) {
|
||||
hasCellWidthHint = parseHtmlWidthAttr(atts[i + 1], cellWidthHint);
|
||||
} else if (strcmp(atts[i], "style") == 0) {
|
||||
cellStyleAttr = atts[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSS width (inline style or stylesheet) overrides HTML attribute
|
||||
if (self->cssParser) {
|
||||
std::string classAttr;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "class") == 0) {
|
||||
classAttr = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
CssStyle cellCssStyle = self->cssParser->resolveStyle(name, classAttr);
|
||||
if (!cellStyleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(cellStyleAttr);
|
||||
cellCssStyle.applyOver(inlineStyle);
|
||||
}
|
||||
if (cellCssStyle.hasWidth()) {
|
||||
cellWidthHint = cellCssStyle.width;
|
||||
hasCellWidthHint = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there's a row to add cells to
|
||||
if (self->tableData->rows.empty()) {
|
||||
self->tableData->rows.push_back(TableRow());
|
||||
}
|
||||
|
||||
// Create a new ParsedText for this cell (characterData will flow into it)
|
||||
auto cellBlockStyle = BlockStyle();
|
||||
cellBlockStyle.alignment = CssTextAlign::Left;
|
||||
cellBlockStyle.textAlignDefined = true;
|
||||
// Explicitly disable paragraph indent for table cells
|
||||
cellBlockStyle.textIndent = 0;
|
||||
cellBlockStyle.textIndentDefined = true;
|
||||
self->currentTextBlock.reset(
|
||||
new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, cellBlockStyle));
|
||||
self->nextWordContinues = false;
|
||||
|
||||
// Track the cell
|
||||
auto& currentRow = self->tableData->rows.back();
|
||||
currentRow.cells.push_back(TableCell());
|
||||
currentRow.cells.back().isHeader = isHeader;
|
||||
currentRow.cells.back().colspan = colspan;
|
||||
if (hasCellWidthHint) {
|
||||
currentRow.cells.back().widthHint = cellWidthHint;
|
||||
currentRow.cells.back().hasWidthHint = true;
|
||||
}
|
||||
|
||||
// Apply bold for header cells
|
||||
if (isHeader) {
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Transparent table container tags
|
||||
if (matches(name, TABLE_TRANSPARENT_TAGS, NUM_TABLE_TRANSPARENT_TAGS)) {
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip colgroup, col, caption
|
||||
if (matches(name, TABLE_SKIP_TAGS, NUM_TABLE_SKIP_TAGS)) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Other tags inside table cells (p, div, span, b, i, etc.) fall through
|
||||
// to the normal handling below. startNewTextBlock is a no-op when inTable.
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
std::string src;
|
||||
std::string alt;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "src") == 0) {
|
||||
src = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "alt") == 0) {
|
||||
alt = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.empty()) {
|
||||
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||
|
||||
{
|
||||
// Resolve the image path relative to the HTML file
|
||||
std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src);
|
||||
|
||||
// Create a unique filename for the cached image
|
||||
std::string ext;
|
||||
size_t extPos = resolvedPath.rfind('.');
|
||||
if (extPos != std::string::npos) {
|
||||
ext = resolvedPath.substr(extPos);
|
||||
}
|
||||
std::string cachedImagePath = self->imageBasePath + std::to_string(self->imageCounter++) + ext;
|
||||
|
||||
// Extract image to cache file
|
||||
FsFile cachedImageFile;
|
||||
bool extractSuccess = false;
|
||||
if (Storage.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) {
|
||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||
cachedImageFile.flush();
|
||||
cachedImageFile.close();
|
||||
delay(50); // Give SD card time to sync
|
||||
}
|
||||
|
||||
if (extractSuccess) {
|
||||
// Get image dimensions
|
||||
ImageDimensions dims = {0, 0};
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
||||
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||
|
||||
// Scale to fit viewport while maintaining aspect ratio
|
||||
int maxWidth = self->viewportWidth;
|
||||
int maxHeight = self->viewportHeight;
|
||||
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
||||
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
|
||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (scale > 1.0f) scale = 1.0f;
|
||||
|
||||
int displayWidth = (int)(dims.width * scale);
|
||||
int displayHeight = (int)(dims.height * scale);
|
||||
|
||||
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
|
||||
|
||||
// Create page for image - only break if image won't fit remaining space
|
||||
if (self->currentPage && !self->currentPage->elements.empty() &&
|
||||
(self->currentPageNextY + displayHeight > self->viewportHeight)) {
|
||||
self->completePageFn(std::move(self->currentPage));
|
||||
self->currentPage.reset(new Page());
|
||||
if (!self->currentPage) {
|
||||
LOG_ERR("EHP", "Failed to create new page");
|
||||
return;
|
||||
}
|
||||
self->currentPageNextY = 0;
|
||||
} else if (!self->currentPage) {
|
||||
self->currentPage.reset(new Page());
|
||||
if (!self->currentPage) {
|
||||
LOG_ERR("EHP", "Failed to create initial page");
|
||||
return;
|
||||
}
|
||||
self->currentPageNextY = 0;
|
||||
}
|
||||
|
||||
// Create ImageBlock and add to page
|
||||
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight);
|
||||
if (!imageBlock) {
|
||||
LOG_ERR("EHP", "Failed to create ImageBlock");
|
||||
return;
|
||||
}
|
||||
int xPos = (self->viewportWidth - displayWidth) / 2;
|
||||
auto pageImage = std::make_shared<PageImage>(imageBlock, xPos, self->currentPageNextY);
|
||||
if (!pageImage) {
|
||||
LOG_ERR("EHP", "Failed to create PageImage");
|
||||
return;
|
||||
}
|
||||
self->currentPage->elements.push_back(pageImage);
|
||||
self->currentPageNextY += displayHeight;
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
} else {
|
||||
LOG_ERR("EHP", "Failed to get image dimensions");
|
||||
Storage.remove(cachedImagePath.c_str());
|
||||
}
|
||||
} else {
|
||||
LOG_ERR("EHP", "Failed to extract image");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to alt text if image processing fails
|
||||
if (!alt.empty()) {
|
||||
alt = "[Image: " + alt + "]";
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
// Skip any child content (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// No alt text, skip
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||
@@ -359,6 +670,28 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect U+00A0 (non-breaking space): UTF-8 encoding is 0xC2 0xA0
|
||||
// Render a visible space without allowing a line break around it.
|
||||
if (static_cast<uint8_t>(s[i]) == 0xC2 && i + 1 < len && static_cast<uint8_t>(s[i + 1]) == 0xA0) {
|
||||
// Flush any pending text so style is applied correctly.
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
|
||||
// Add a standalone space that attaches to the previous word.
|
||||
self->partWordBuffer[0] = ' ';
|
||||
self->partWordBuffer[1] = '\0';
|
||||
self->partWordBufferIndex = 1;
|
||||
self->nextWordContinues = true; // Attach space to previous word (no break).
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
// Ensure the next real word attaches to this space (no break).
|
||||
self->nextWordContinues = true;
|
||||
|
||||
i++; // Skip the second byte (0xA0)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
|
||||
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
|
||||
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
|
||||
@@ -385,14 +718,31 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
||||
// memory.
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
if (self->currentTextBlock->size() > 750) {
|
||||
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
|
||||
// Skip this when inside a table - cell content is buffered for later layout.
|
||||
if (!self->inTable && self->currentTextBlock->size() > 750) {
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
self->currentTextBlock->layoutAndExtractLines(
|
||||
self->renderer, self->fontId, self->viewportWidth,
|
||||
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
|
||||
// Check if this looks like an entity reference (&...;)
|
||||
if (len >= 3 && s[0] == '&' && s[len - 1] == ';') {
|
||||
const char* utf8Value = lookupHtmlEntity(s, len);
|
||||
if (utf8Value != nullptr) {
|
||||
// Known entity: expand to its UTF-8 value
|
||||
characterData(userData, utf8Value, strlen(utf8Value));
|
||||
return;
|
||||
}
|
||||
// Unknown entity: preserve original &...; sequence
|
||||
characterData(userData, s, len);
|
||||
return;
|
||||
}
|
||||
// Not an entity we recognize - skip it
|
||||
}
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
@@ -407,15 +757,17 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
|
||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||
const bool isTableCellTag = strcmp(name, "td") == 0 || strcmp(name, "th") == 0;
|
||||
const bool isTableTag = strcmp(name, "table") == 0;
|
||||
|
||||
// Flush buffer with current style BEFORE any style changes
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// Flush if style will change OR if we're closing a block/structural element
|
||||
const bool isInlineTag = !headerOrBlockTag && strcmp(name, "table") != 0 &&
|
||||
const bool isInlineTag = !headerOrBlockTag && !isTableTag && !isTableCellTag &&
|
||||
!matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
|
||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || isTableTag || isTableCellTag ||
|
||||
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||
|
||||
if (shouldFlush) {
|
||||
@@ -427,6 +779,57 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
}
|
||||
}
|
||||
|
||||
// --- Table cell/row/table close handling ---
|
||||
if (self->inTable) {
|
||||
if (isTableCellTag) {
|
||||
// Save the current cell content into the table data
|
||||
if (self->tableData && !self->tableData->rows.empty()) {
|
||||
auto& currentRow = self->tableData->rows.back();
|
||||
if (!currentRow.cells.empty()) {
|
||||
currentRow.cells.back().content = std::move(self->currentTextBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a safe empty ParsedText so character data between cells doesn't crash
|
||||
auto safeBlockStyle = BlockStyle();
|
||||
safeBlockStyle.alignment = CssTextAlign::Left;
|
||||
self->currentTextBlock.reset(
|
||||
new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, safeBlockStyle));
|
||||
self->nextWordContinues = false;
|
||||
}
|
||||
|
||||
if (isTableTag) {
|
||||
// Process the entire buffered table
|
||||
self->depth -= 1;
|
||||
|
||||
// Clean up style state for this depth
|
||||
if (self->skipUntilDepth == self->depth) self->skipUntilDepth = INT_MAX;
|
||||
if (self->boldUntilDepth == self->depth) self->boldUntilDepth = INT_MAX;
|
||||
if (self->italicUntilDepth == self->depth) self->italicUntilDepth = INT_MAX;
|
||||
if (self->underlineUntilDepth == self->depth) self->underlineUntilDepth = INT_MAX;
|
||||
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
||||
self->inlineStyleStack.pop_back();
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
|
||||
self->processTable();
|
||||
|
||||
self->inTable = false;
|
||||
self->tableData.reset();
|
||||
|
||||
// Restore a fresh text block for content after the table
|
||||
auto paragraphAlignmentBlockStyle = BlockStyle();
|
||||
paragraphAlignmentBlockStyle.textAlignDefined = true;
|
||||
const auto align = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
|
||||
? CssTextAlign::Justify
|
||||
: static_cast<CssTextAlign>(self->paragraphAlignment);
|
||||
paragraphAlignmentBlockStyle.alignment = align;
|
||||
self->currentTextBlock.reset(
|
||||
new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, paragraphAlignmentBlockStyle));
|
||||
return; // depth already decremented, skip the normal endElement cleanup
|
||||
}
|
||||
}
|
||||
|
||||
self->depth -= 1;
|
||||
|
||||
// Leaving skip
|
||||
@@ -477,12 +880,16 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
int done;
|
||||
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
||||
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
||||
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
||||
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand);
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("EHP", filepath, file)) {
|
||||
if (!Storage.openFileForRead("EHP", filepath, file)) {
|
||||
XML_ParserFree(parser);
|
||||
return false;
|
||||
}
|
||||
@@ -499,7 +906,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
do {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
||||
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -511,7 +918,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
const size_t len = file.read(buf, 1024);
|
||||
|
||||
if (len == 0 && file.available() > 0) {
|
||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||
LOG_ERR("EHP", "File read error");
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -523,8 +930,8 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
done = file.available() == 0;
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -568,7 +975,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
||||
|
||||
void ChapterHtmlSlimParser::makePages() {
|
||||
if (!currentTextBlock) {
|
||||
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
||||
LOG_ERR("EHP", "!! No text block to make pages for !!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -610,3 +1017,335 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
currentPageNextY += lineHeight / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table processing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Cell padding in pixels (horizontal space between grid line and cell text)
|
||||
static constexpr int TABLE_CELL_PAD_X = 4;
|
||||
// Vertical cell padding — asymmetric because font metrics include internal leading (whitespace
|
||||
// above glyphs), so the top already has built-in visual space. Less explicit padding on top,
|
||||
// more on bottom, produces visually balanced results.
|
||||
static constexpr int TABLE_CELL_PAD_TOP = 1;
|
||||
static constexpr int TABLE_CELL_PAD_BOTTOM = 3;
|
||||
// Minimum usable column width in pixels (below this text is unreadable)
|
||||
static constexpr int TABLE_MIN_COL_WIDTH = 30;
|
||||
// Grid line width in pixels
|
||||
static constexpr int TABLE_GRID_LINE_PX = 1;
|
||||
|
||||
void ChapterHtmlSlimParser::addTableRowToPage(std::shared_ptr<PageTableRow> row) {
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
const int16_t rowH = row->getHeight();
|
||||
|
||||
// If this row doesn't fit on the current page, start a new one
|
||||
if (currentPageNextY + rowH > viewportHeight) {
|
||||
completePageFn(std::move(currentPage));
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
row->xPos = 0;
|
||||
row->yPos = currentPageNextY;
|
||||
currentPage->elements.push_back(std::move(row));
|
||||
currentPageNextY += rowH;
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::processTable() {
|
||||
if (!tableData || tableData->rows.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
const int lh = static_cast<int>(renderer.getLineHeight(fontId) * lineCompression);
|
||||
|
||||
// 1. Determine logical column count using colspan.
|
||||
// Each cell occupies cell.colspan logical columns. The total for a row is the sum of colspans.
|
||||
size_t numCols = 0;
|
||||
for (const auto& row : tableData->rows) {
|
||||
size_t rowLogicalCols = 0;
|
||||
for (const auto& cell : row.cells) {
|
||||
rowLogicalCols += static_cast<size_t>(cell.colspan);
|
||||
}
|
||||
numCols = std::max(numCols, rowLogicalCols);
|
||||
}
|
||||
|
||||
if (numCols == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Measure natural width of each cell and compute per-column max natural width.
|
||||
// Only non-spanning cells (colspan==1) contribute to individual column widths.
|
||||
// Spanning cells use the combined width of their spanned columns.
|
||||
std::vector<uint16_t> colNaturalWidth(numCols, 0);
|
||||
|
||||
for (const auto& row : tableData->rows) {
|
||||
size_t logicalCol = 0;
|
||||
for (const auto& cell : row.cells) {
|
||||
if (cell.colspan == 1 && cell.content && !cell.content->isEmpty()) {
|
||||
if (logicalCol < numCols) {
|
||||
const uint16_t w = cell.content->getNaturalWidth(renderer, fontId);
|
||||
if (w > colNaturalWidth[logicalCol]) {
|
||||
colNaturalWidth[logicalCol] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
logicalCol += static_cast<size_t>(cell.colspan);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calculate column widths to fit viewport.
|
||||
// Available width = viewport - outer borders - internal column borders - cell padding
|
||||
const int totalGridLines = static_cast<int>(numCols) + 1; // left + between columns + right
|
||||
const int totalPadding = static_cast<int>(numCols) * TABLE_CELL_PAD_X * 2;
|
||||
const int availableForContent = viewportWidth - totalGridLines * TABLE_GRID_LINE_PX - totalPadding;
|
||||
|
||||
// 3a. Resolve width hints per column.
|
||||
// Priority: <col> hints > max cell hint (colspan=1 only).
|
||||
// Percentages are relative to availableForContent.
|
||||
const float emSize = static_cast<float>(lh);
|
||||
const float containerW = static_cast<float>(std::max(availableForContent, 0));
|
||||
|
||||
std::vector<int> colHintedWidth(numCols, -1); // -1 = no hint
|
||||
|
||||
// From <col> tags
|
||||
for (size_t c = 0; c < numCols && c < tableData->colWidthHints.size(); ++c) {
|
||||
const auto& hint = tableData->colWidthHints[c];
|
||||
if (hint.value > 0) {
|
||||
int px = static_cast<int>(hint.toPixels(emSize, containerW));
|
||||
if (px > 0) {
|
||||
colHintedWidth[c] = std::max(px, TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From <td>/<th> cell width hints (only override if no <col> hint exists for this column)
|
||||
for (const auto& row : tableData->rows) {
|
||||
size_t logicalCol = 0;
|
||||
for (const auto& cell : row.cells) {
|
||||
if (cell.colspan == 1 && cell.hasWidthHint && logicalCol < numCols) {
|
||||
if (colHintedWidth[logicalCol] < 0) { // no <col> hint yet
|
||||
int px = static_cast<int>(cell.widthHint.toPixels(emSize, containerW));
|
||||
if (px > colHintedWidth[logicalCol]) {
|
||||
colHintedWidth[logicalCol] = std::max(px, TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
logicalCol += static_cast<size_t>(cell.colspan);
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Distribute column widths: hinted columns get their hint, unhinted use auto-sizing.
|
||||
std::vector<uint16_t> colWidths(numCols, 0);
|
||||
|
||||
if (availableForContent <= 0) {
|
||||
const uint16_t equalWidth = static_cast<uint16_t>(viewportWidth / numCols);
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
colWidths[c] = equalWidth;
|
||||
}
|
||||
} else {
|
||||
// First, assign hinted columns and track how much space they consume
|
||||
int hintedSpaceUsed = 0;
|
||||
size_t unhintedCount = 0;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
hintedSpaceUsed += colHintedWidth[c];
|
||||
} else {
|
||||
unhintedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If hinted columns exceed available space, scale them down proportionally
|
||||
if (hintedSpaceUsed > availableForContent && hintedSpaceUsed > 0) {
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
colHintedWidth[c] = colHintedWidth[c] * availableForContent / hintedSpaceUsed;
|
||||
colHintedWidth[c] = std::max(colHintedWidth[c], TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
// Recalculate
|
||||
hintedSpaceUsed = 0;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
hintedSpaceUsed += colHintedWidth[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign hinted columns
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
colWidths[c] = static_cast<uint16_t>(colHintedWidth[c]);
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute remaining space among unhinted columns using the existing algorithm
|
||||
const int remainingForUnhinted = std::max(availableForContent - hintedSpaceUsed, 0);
|
||||
|
||||
if (unhintedCount > 0 && remainingForUnhinted > 0) {
|
||||
// Compute total natural width of unhinted columns
|
||||
int totalNaturalUnhinted = 0;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
totalNaturalUnhinted += colNaturalWidth[c];
|
||||
}
|
||||
}
|
||||
|
||||
if (totalNaturalUnhinted <= remainingForUnhinted) {
|
||||
// All unhinted content fits — distribute extra space equally among unhinted columns
|
||||
const int extraSpace = remainingForUnhinted - totalNaturalUnhinted;
|
||||
const int perColExtra = extraSpace / static_cast<int>(unhintedCount);
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
colWidths[c] = static_cast<uint16_t>(colNaturalWidth[c] + perColExtra);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unhinted content exceeds remaining space — two-pass fair-share among unhinted columns
|
||||
const int equalShare = remainingForUnhinted / static_cast<int>(unhintedCount);
|
||||
|
||||
int spaceUsedByFitting = 0;
|
||||
int naturalOfWide = 0;
|
||||
size_t wideCount = 0;
|
||||
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
if (static_cast<int>(colNaturalWidth[c]) <= equalShare) {
|
||||
colWidths[c] = colNaturalWidth[c];
|
||||
spaceUsedByFitting += colNaturalWidth[c];
|
||||
} else {
|
||||
naturalOfWide += colNaturalWidth[c];
|
||||
wideCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int wideSpace = remainingForUnhinted - spaceUsedByFitting;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0 && static_cast<int>(colNaturalWidth[c]) > equalShare) {
|
||||
if (naturalOfWide > 0 && wideCount > 1) {
|
||||
int proportional = static_cast<int>(colNaturalWidth[c]) * wideSpace / naturalOfWide;
|
||||
colWidths[c] = static_cast<uint16_t>(std::max(proportional, TABLE_MIN_COL_WIDTH));
|
||||
} else {
|
||||
colWidths[c] = static_cast<uint16_t>(std::max(wideSpace, TABLE_MIN_COL_WIDTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (unhintedCount > 0) {
|
||||
// No remaining space for unhinted columns — give them minimum width
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
colWidths[c] = static_cast<uint16_t>(TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute column x-offsets (cumulative: border + padding + content width + padding + border ...)
|
||||
std::vector<uint16_t> colXOffsets(numCols, 0);
|
||||
int xAccum = TABLE_GRID_LINE_PX; // start after left border
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
colXOffsets[c] = static_cast<uint16_t>(xAccum);
|
||||
xAccum += TABLE_CELL_PAD_X + colWidths[c] + TABLE_CELL_PAD_X + TABLE_GRID_LINE_PX;
|
||||
}
|
||||
const int16_t totalTableWidth = static_cast<int16_t>(xAccum);
|
||||
|
||||
// Helper: compute the combined content width for a cell spanning multiple columns.
|
||||
// This includes the content widths plus the internal grid lines and padding between spanned columns.
|
||||
auto spanContentWidth = [&](size_t startCol, int colspan) -> uint16_t {
|
||||
int width = 0;
|
||||
for (int s = 0; s < colspan && startCol + s < numCols; ++s) {
|
||||
width += colWidths[startCol + s];
|
||||
if (s > 0) {
|
||||
// Add internal padding and grid line between spanned columns
|
||||
width += TABLE_CELL_PAD_X * 2 + TABLE_GRID_LINE_PX;
|
||||
}
|
||||
}
|
||||
return static_cast<uint16_t>(std::max(width, 0));
|
||||
};
|
||||
|
||||
// Helper: compute the full cell width (including padding on both sides) for a spanning cell.
|
||||
auto spanFullCellWidth = [&](size_t startCol, int colspan) -> uint16_t {
|
||||
if (colspan <= 0 || startCol >= numCols) return 0;
|
||||
const size_t endCol = std::min(startCol + static_cast<size_t>(colspan), numCols) - 1;
|
||||
// From the left edge of startCol's cell to the right edge of endCol's cell
|
||||
const int leftEdge = colXOffsets[startCol];
|
||||
const int rightEdge = colXOffsets[endCol] + TABLE_CELL_PAD_X + colWidths[endCol] + TABLE_CELL_PAD_X;
|
||||
return static_cast<uint16_t>(rightEdge - leftEdge);
|
||||
};
|
||||
|
||||
// 4. Lay out each row: map cells to logical columns, create PageTableRow
|
||||
for (auto& row : tableData->rows) {
|
||||
// Build cell data for this row, one entry per CELL (not per logical column).
|
||||
// Each PageTableCellData gets the correct x-offset and combined column width.
|
||||
std::vector<PageTableCellData> cellDataVec;
|
||||
size_t maxLinesInRow = 1;
|
||||
size_t logicalCol = 0;
|
||||
|
||||
for (size_t ci = 0; ci < row.cells.size() && logicalCol < numCols; ++ci) {
|
||||
auto& cell = row.cells[ci];
|
||||
const int cs = cell.colspan;
|
||||
|
||||
PageTableCellData cellData;
|
||||
cellData.xOffset = colXOffsets[logicalCol];
|
||||
cellData.columnWidth = spanFullCellWidth(logicalCol, cs);
|
||||
|
||||
if (cell.content && !cell.content->isEmpty()) {
|
||||
// Center-align cells that span the full table width (common for section headers/titles)
|
||||
if (cs >= static_cast<int>(numCols)) {
|
||||
BlockStyle centeredStyle = cell.content->getBlockStyle();
|
||||
centeredStyle.alignment = CssTextAlign::Center;
|
||||
centeredStyle.textAlignDefined = true;
|
||||
cell.content->setBlockStyle(centeredStyle);
|
||||
}
|
||||
|
||||
const uint16_t contentWidth = spanContentWidth(logicalCol, cs);
|
||||
std::vector<std::shared_ptr<TextBlock>> cellLines;
|
||||
|
||||
cell.content->layoutAndExtractLines(
|
||||
renderer, fontId, contentWidth,
|
||||
[&cellLines](const std::shared_ptr<TextBlock>& textBlock) { cellLines.push_back(textBlock); });
|
||||
|
||||
if (cellLines.size() > maxLinesInRow) {
|
||||
maxLinesInRow = cellLines.size();
|
||||
}
|
||||
cellData.lines = std::move(cellLines);
|
||||
}
|
||||
|
||||
cellDataVec.push_back(std::move(cellData));
|
||||
logicalCol += static_cast<size_t>(cs);
|
||||
}
|
||||
|
||||
// Fill remaining logical columns with empty cells (rows shorter than numCols)
|
||||
while (logicalCol < numCols) {
|
||||
PageTableCellData emptyCell;
|
||||
emptyCell.xOffset = colXOffsets[logicalCol];
|
||||
emptyCell.columnWidth = static_cast<uint16_t>(TABLE_CELL_PAD_X + colWidths[logicalCol] + TABLE_CELL_PAD_X);
|
||||
cellDataVec.push_back(std::move(emptyCell));
|
||||
logicalCol++;
|
||||
}
|
||||
|
||||
// Row height = max lines * lineHeight + top/bottom border + asymmetric vertical padding
|
||||
const int16_t rowHeight = static_cast<int16_t>(
|
||||
static_cast<int>(maxLinesInRow) * lh + 2 + TABLE_CELL_PAD_TOP + TABLE_CELL_PAD_BOTTOM);
|
||||
|
||||
auto pageTableRow = std::make_shared<PageTableRow>(
|
||||
std::move(cellDataVec), rowHeight, totalTableWidth, static_cast<int16_t>(lh), 0, 0);
|
||||
|
||||
addTableRowToPage(std::move(pageTableRow));
|
||||
}
|
||||
|
||||
// Add a small gap after the table
|
||||
if (extraParagraphSpacing) {
|
||||
currentPageNextY += lh / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,21 @@
|
||||
#include <memory>
|
||||
|
||||
#include "../ParsedText.h"
|
||||
#include "../TableData.h"
|
||||
#include "../blocks/ImageBlock.h"
|
||||
#include "../blocks/TextBlock.h"
|
||||
#include "../css/CssParser.h"
|
||||
#include "../css/CssStyle.h"
|
||||
|
||||
class Page;
|
||||
class PageTableRow;
|
||||
class GfxRenderer;
|
||||
class Epub;
|
||||
|
||||
#define MAX_WORD_SIZE 200
|
||||
|
||||
class ChapterHtmlSlimParser {
|
||||
std::shared_ptr<Epub> epub;
|
||||
const std::string& filepath;
|
||||
GfxRenderer& renderer;
|
||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||
@@ -43,6 +48,9 @@ class ChapterHtmlSlimParser {
|
||||
bool hyphenationEnabled;
|
||||
const CssParser* cssParser;
|
||||
bool embeddedStyle;
|
||||
std::string contentBase;
|
||||
std::string imageBasePath;
|
||||
int imageCounter = 0;
|
||||
|
||||
// Style tracking (replaces depth-based approach)
|
||||
struct StyleStackEntry {
|
||||
@@ -57,25 +65,34 @@ class ChapterHtmlSlimParser {
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
|
||||
// Table buffering state
|
||||
bool inTable = false;
|
||||
std::unique_ptr<TableData> tableData;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
void processTable();
|
||||
void addTableRowToPage(std::shared_ptr<PageTableRow> row);
|
||||
// XML callbacks
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
||||
const float lineCompression, const bool extraParagraphSpacing,
|
||||
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
|
||||
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 std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||
const bool embeddedStyle, const std::function<void()>& popupFn = nullptr,
|
||||
const bool embeddedStyle, const std::string& contentBase,
|
||||
const std::string& imageBasePath, const std::function<void()>& popupFn = nullptr,
|
||||
const CssParser* cssParser = nullptr)
|
||||
|
||||
: filepath(filepath),
|
||||
: epub(epub),
|
||||
filepath(filepath),
|
||||
renderer(renderer),
|
||||
fontId(fontId),
|
||||
lineCompression(lineCompression),
|
||||
@@ -87,7 +104,9 @@ class ChapterHtmlSlimParser {
|
||||
completePageFn(completePageFn),
|
||||
popupFn(popupFn),
|
||||
cssParser(cssParser),
|
||||
embeddedStyle(embeddedStyle) {}
|
||||
embeddedStyle(embeddedStyle),
|
||||
contentBase(contentBase),
|
||||
imageBasePath(imageBasePath) {}
|
||||
|
||||
~ChapterHtmlSlimParser() = default;
|
||||
bool parseAndBuildPages();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "ContainerParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
bool ContainerParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis());
|
||||
LOG_ERR("CTR", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis());
|
||||
LOG_DBG("CTR", "Couldn't allocate buffer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
LOG_ERR("CTR", "Parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "ContentOpfParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "../BookMetadataCache.h"
|
||||
@@ -15,7 +15,7 @@ constexpr char itemCacheFile[] = "/.items.bin";
|
||||
bool ContentOpfParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis());
|
||||
LOG_DBG("COF", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ ContentOpfParser::~ContentOpfParser() {
|
||||
if (tempItemStore) {
|
||||
tempItemStore.close();
|
||||
}
|
||||
if (SdMan.exists((cachePath + itemCacheFile).c_str())) {
|
||||
SdMan.remove((cachePath + itemCacheFile).c_str());
|
||||
if (Storage.exists((cachePath + itemCacheFile).c_str())) {
|
||||
Storage.remove((cachePath + itemCacheFile).c_str());
|
||||
}
|
||||
itemIndex.clear();
|
||||
itemIndex.shrink_to_fit();
|
||||
@@ -56,7 +56,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
|
||||
LOG_ERR("COF", "Couldn't allocate memory for buffer");
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -69,8 +69,8 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
LOG_DBG("COF", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -118,20 +118,16 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||
self->state = IN_MANIFEST;
|
||||
if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
Serial.printf(
|
||||
"[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n",
|
||||
millis());
|
||||
if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
LOG_ERR("COF", "Couldn't open temp items file for writing. This is probably going to be a fatal error.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||
self->state = IN_SPINE;
|
||||
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
Serial.printf(
|
||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
||||
millis());
|
||||
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
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
|
||||
@@ -140,7 +136,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
self->useItemIndex = true;
|
||||
Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size());
|
||||
LOG_DBG("COF", "Using fast index for %zu manifest items", self->itemIndex.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -148,11 +144,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||
self->state = IN_GUIDE;
|
||||
// TODO Remove print
|
||||
Serial.printf("[%lu] [COF] Entering guide state.\n", millis());
|
||||
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
Serial.printf(
|
||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
||||
millis());
|
||||
LOG_DBG("COF", "Entering guide state.");
|
||||
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -214,8 +208,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
if (self->tocNcxPath.empty()) {
|
||||
self->tocNcxPath = href;
|
||||
} else {
|
||||
Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(),
|
||||
href.c_str());
|
||||
LOG_DBG("COF", "Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +222,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
// Properties is space-separated, check if "nav" is present as a word
|
||||
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
||||
self->tocNavPath = href;
|
||||
Serial.printf("[%lu] [COF] Found EPUB 3 nav document: %s\n", millis(), href.c_str());
|
||||
LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// EPUB 3: Check for cover image (properties contains "cover-image")
|
||||
if (!properties.empty() && self->coverItemHref.empty()) {
|
||||
if (properties == "cover-image" || properties.find("cover-image ") == 0 ||
|
||||
properties.find(" cover-image") != std::string::npos) {
|
||||
self->coverItemHref = href;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -302,7 +303,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
if (type == "text" || type == "start") {
|
||||
continue;
|
||||
} else {
|
||||
Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str());
|
||||
LOG_DBG("COF", "Skipping non-text reference in guide: %s", type.c_str());
|
||||
break;
|
||||
}
|
||||
} else if (strcmp(atts[i], "href") == 0) {
|
||||
@@ -310,7 +311,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
}
|
||||
}
|
||||
if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) {
|
||||
Serial.printf("[%lu] [COF] Found %s reference in guide: %s.\n", millis(), type.c_str(), textHref.c_str());
|
||||
LOG_DBG("COF", "Found %s reference in guide: %s.", type.c_str(), textHref.c_str());
|
||||
self->textReferenceHref = textHref;
|
||||
}
|
||||
return;
|
||||
@@ -326,6 +327,9 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s,
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_AUTHOR) {
|
||||
if (!self->author.empty()) {
|
||||
self->author.append(", "); // Add separator for multiple authors
|
||||
}
|
||||
self->author.append(s, len);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "TocNavParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "../BookMetadataCache.h"
|
||||
|
||||
bool TocNavParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [NAV] Couldn't allocate memory for parser\n", millis());
|
||||
LOG_DBG("NAV", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [NAV] Couldn't allocate memory for buffer\n", millis());
|
||||
LOG_DBG("NAV", "Couldn't allocate memory for buffer");
|
||||
XML_StopParser(parser, XML_FALSE);
|
||||
XML_SetElementHandler(parser, nullptr, nullptr);
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -52,8 +52,8 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [NAV] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
LOG_DBG("NAV", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE);
|
||||
XML_SetElementHandler(parser, nullptr, nullptr);
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -88,7 +88,7 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
||||
self->state = IN_NAV_TOC;
|
||||
Serial.printf("[%lu] [NAV] Found nav toc element\n", millis());
|
||||
LOG_DBG("NAV", "Found nav toc element");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
|
||||
|
||||
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
||||
self->state = IN_BODY;
|
||||
Serial.printf("[%lu] [NAV] Finished parsing nav toc\n", millis());
|
||||
LOG_DBG("NAV", "Finished parsing nav toc");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "TocNcxParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "../BookMetadataCache.h"
|
||||
|
||||
bool TocNcxParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis());
|
||||
LOG_DBG("TOC", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
|
||||
LOG_DBG("TOC", "Couldn't allocate memory for buffer");
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
@@ -52,8 +52,8 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
LOG_DBG("TOC", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
@@ -104,3 +104,20 @@ uint8_t quantize1bit(int gray, int x, int y) {
|
||||
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
|
||||
return (gray >= adjustedThreshold) ? 1 : 0;
|
||||
}
|
||||
|
||||
// Noise dithering for gradient fills - always uses hash-based noise regardless of global dithering config.
|
||||
// Produces smooth-looking gradients on the 4-level e-ink display.
|
||||
uint8_t quantizeNoiseDither(int gray, int x, int y) {
|
||||
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||
const int threshold = static_cast<int>(hash >> 24);
|
||||
|
||||
const int scaled = gray * 3;
|
||||
if (scaled < 255) {
|
||||
return (scaled + threshold >= 255) ? 1 : 0;
|
||||
} else if (scaled < 510) {
|
||||
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
||||
} else {
|
||||
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ uint8_t quantize(int gray, int x, int y);
|
||||
uint8_t quantizeSimple(int gray);
|
||||
uint8_t quantize1bit(int gray, int x, int y);
|
||||
int adjustPixel(int gray);
|
||||
uint8_t quantizeNoiseDither(int gray, int x, int y);
|
||||
|
||||
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
|
||||
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
void GfxRenderer::begin() {
|
||||
frameBuffer = display.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
||||
LOG_ERR("GFX", "!! No framebuffer");
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +58,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
|
||||
// Bounds checking against physical panel dimensions
|
||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, phyX, phyY);
|
||||
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,9 +73,19 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawPixelGray(const int x, const int y, const uint8_t val2bit) const {
|
||||
if (renderMode == BW && val2bit < 3) {
|
||||
drawPixel(x, y);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val2bit == 1 || val2bit == 2)) {
|
||||
drawPixel(x, y, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val2bit == 1) {
|
||||
drawPixel(x, y, false);
|
||||
}
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -100,7 +111,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
}
|
||||
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
@@ -133,7 +144,7 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con
|
||||
}
|
||||
} else {
|
||||
// TODO: Implement
|
||||
Serial.printf("[%lu] [GFX] Line drawing not supported\n", millis());
|
||||
LOG_ERR("GFX", "Line drawing not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,18 +430,26 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
bool isScaled = false;
|
||||
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
||||
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
||||
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
||||
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
|
||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||
const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth();
|
||||
const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight();
|
||||
|
||||
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
||||
if (maxWidth > 0 && maxHeight > 0) {
|
||||
const float scaleX = static_cast<float>(maxWidth) / effectiveWidth;
|
||||
const float scaleY = static_cast<float>(maxHeight) / effectiveHeight;
|
||||
scale = std::min(scaleX, scaleY);
|
||||
isScaled = (scale < 0.999f || scale > 1.001f);
|
||||
} else if (maxWidth > 0 && effectiveWidth > static_cast<float>(maxWidth)) {
|
||||
scale = static_cast<float>(maxWidth) / effectiveWidth;
|
||||
isScaled = true;
|
||||
} else if (maxHeight > 0 && effectiveHeight > static_cast<float>(maxHeight)) {
|
||||
scale = static_cast<float>(maxHeight) / effectiveHeight;
|
||||
isScaled = true;
|
||||
}
|
||||
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||
isScaled = true;
|
||||
}
|
||||
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
||||
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
||||
|
||||
// Calculate output row size (2 bits per pixel, packed into bytes)
|
||||
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
||||
@@ -439,7 +458,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||
|
||||
if (!outputRow || !rowBytes) {
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
|
||||
LOG_ERR("GFX", "!! Failed to allocate BMP row buffers");
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return;
|
||||
@@ -448,23 +467,28 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
||||
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
||||
// Screen's (0, 0) is the top-left corner.
|
||||
int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||
const int logicalY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||
int screenYStart, screenYEnd;
|
||||
if (isScaled) {
|
||||
screenY = std::floor(screenY * scale);
|
||||
screenYStart = static_cast<int>(std::floor(logicalY * scale)) + y;
|
||||
screenYEnd = static_cast<int>(std::floor((logicalY + 1) * scale)) + y;
|
||||
} else {
|
||||
screenYStart = logicalY + y;
|
||||
screenYEnd = screenYStart + 1;
|
||||
}
|
||||
screenY += y; // the offset should not be scaled
|
||||
if (screenY >= getScreenHeight()) {
|
||||
|
||||
if (screenYStart >= getScreenHeight()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
|
||||
LOG_ERR("GFX", "Failed to read row %d from bitmap", bmpY);
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenY < 0) {
|
||||
if (screenYEnd <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -473,27 +497,42 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
continue;
|
||||
}
|
||||
|
||||
const int syStart = std::max(screenYStart, 0);
|
||||
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
||||
|
||||
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||
int screenX = bmpX - cropPixX;
|
||||
const int outX = bmpX - cropPixX;
|
||||
int screenXStart, screenXEnd;
|
||||
if (isScaled) {
|
||||
screenX = std::floor(screenX * scale);
|
||||
screenXStart = static_cast<int>(std::floor(outX * scale)) + x;
|
||||
screenXEnd = static_cast<int>(std::floor((outX + 1) * scale)) + x;
|
||||
} else {
|
||||
screenXStart = outX + x;
|
||||
screenXEnd = screenXStart + 1;
|
||||
}
|
||||
screenX += x; // the offset should not be scaled
|
||||
if (screenX >= getScreenWidth()) {
|
||||
|
||||
if (screenXStart >= getScreenWidth()) {
|
||||
break;
|
||||
}
|
||||
if (screenX < 0) {
|
||||
if (screenXEnd <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
||||
|
||||
if (renderMode == BW && val < 3) {
|
||||
drawPixel(screenX, screenY);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
const int sxStart = std::max(screenXStart, 0);
|
||||
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
||||
|
||||
for (int sy = syStart; sy < syEnd; sy++) {
|
||||
for (int sx = sxStart; sx < sxEnd; sx++) {
|
||||
if (renderMode == BW && val < 3) {
|
||||
drawPixel(sx, sy);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
||||
drawPixel(sx, sy, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||
drawPixel(sx, sy, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,11 +545,16 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
const int maxHeight) const {
|
||||
float scale = 1.0f;
|
||||
bool isScaled = false;
|
||||
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
||||
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
||||
if (maxWidth > 0 && maxHeight > 0) {
|
||||
const float scaleX = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||
const float scaleY = static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight());
|
||||
scale = std::min(scaleX, scaleY);
|
||||
isScaled = (scale < 0.999f || scale > 1.001f);
|
||||
} else if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||
isScaled = true;
|
||||
}
|
||||
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||
} else if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
||||
isScaled = true;
|
||||
}
|
||||
@@ -521,7 +565,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||
|
||||
if (!outputRow || !rowBytes) {
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate 1-bit BMP row buffers\n", millis());
|
||||
LOG_ERR("GFX", "!! Failed to allocate 1-bit BMP row buffers");
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return;
|
||||
@@ -530,7 +574,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||
// Read rows sequentially using readNextRow
|
||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||
Serial.printf("[%lu] [GFX] Failed to read row %d from 1-bit bitmap\n", millis(), bmpY);
|
||||
LOG_ERR("GFX", "Failed to read row %d from 1-bit bitmap", bmpY);
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return;
|
||||
@@ -538,20 +582,37 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
|
||||
// Calculate screen Y based on whether BMP is top-down or bottom-up
|
||||
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||
int screenY = y + (isScaled ? static_cast<int>(std::floor(bmpYOffset * scale)) : bmpYOffset);
|
||||
if (screenY >= getScreenHeight()) {
|
||||
int screenYStart, screenYEnd;
|
||||
if (isScaled) {
|
||||
screenYStart = static_cast<int>(std::floor(bmpYOffset * scale)) + y;
|
||||
screenYEnd = static_cast<int>(std::floor((bmpYOffset + 1) * scale)) + y;
|
||||
} else {
|
||||
screenYStart = bmpYOffset + y;
|
||||
screenYEnd = screenYStart + 1;
|
||||
}
|
||||
if (screenYStart >= getScreenHeight()) {
|
||||
continue; // Continue reading to keep row counter in sync
|
||||
}
|
||||
if (screenY < 0) {
|
||||
if (screenYEnd <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int syStart = std::max(screenYStart, 0);
|
||||
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
||||
|
||||
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
||||
int screenX = x + (isScaled ? static_cast<int>(std::floor(bmpX * scale)) : bmpX);
|
||||
if (screenX >= getScreenWidth()) {
|
||||
int screenXStart, screenXEnd;
|
||||
if (isScaled) {
|
||||
screenXStart = static_cast<int>(std::floor(bmpX * scale)) + x;
|
||||
screenXEnd = static_cast<int>(std::floor((bmpX + 1) * scale)) + x;
|
||||
} else {
|
||||
screenXStart = bmpX + x;
|
||||
screenXEnd = screenXStart + 1;
|
||||
}
|
||||
if (screenXStart >= getScreenWidth()) {
|
||||
break;
|
||||
}
|
||||
if (screenX < 0) {
|
||||
if (screenXEnd <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -561,7 +622,13 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
|
||||
// val < 3 means black pixel (draw it)
|
||||
if (val < 3) {
|
||||
drawPixel(screenX, screenY, true);
|
||||
const int sxStart = std::max(screenXStart, 0);
|
||||
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
||||
for (int sy = syStart; sy < syEnd; sy++) {
|
||||
for (int sx = sxStart; sx < sxEnd; sx++) {
|
||||
drawPixel(sx, sy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// White pixels (val == 3) are not drawn (leave background)
|
||||
}
|
||||
@@ -588,7 +655,7 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
|
||||
// Allocate node buffer for scanline algorithm
|
||||
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
||||
if (!nodeX) {
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate polygon node buffer\n", millis());
|
||||
LOG_ERR("GFX", "!! Failed to allocate polygon node buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -655,10 +722,27 @@ void GfxRenderer::invertScreen() const {
|
||||
|
||||
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
||||
auto elapsed = millis() - start_ms;
|
||||
Serial.printf("[%lu] [GFX] Time = %lu ms from clearScreen to displayBuffer\n", millis(), elapsed);
|
||||
LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed);
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: Display only a rectangular region with specified refresh mode
|
||||
void GfxRenderer::displayWindow(int x, int y, int width, int height,
|
||||
HalDisplay::RefreshMode mode) const {
|
||||
LOG_DBG("GFX", "Displaying window at (%d,%d) size (%dx%d) with mode %d", x, y, width, height,
|
||||
static_cast<int>(mode));
|
||||
|
||||
// Validate bounds
|
||||
if (x < 0 || y < 0 || x + width > getScreenWidth() || y + height > getScreenHeight()) {
|
||||
LOG_ERR("GFX", "Window bounds exceed display dimensions!");
|
||||
return;
|
||||
}
|
||||
|
||||
display.displayWindow(static_cast<uint16_t>(x), static_cast<uint16_t>(y),
|
||||
static_cast<uint16_t>(width), static_cast<uint16_t>(height), mode,
|
||||
fadingFix);
|
||||
}
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
if (!text || maxWidth <= 0) return "";
|
||||
@@ -709,7 +793,7 @@ int GfxRenderer::getScreenHeight() const {
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -718,7 +802,7 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
|
||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -732,7 +816,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -741,7 +825,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
|
||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -750,7 +834,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
|
||||
int GfxRenderer::getTextHeight(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||
@@ -764,7 +848,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
}
|
||||
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
@@ -839,6 +923,92 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
const EpdFontFamily::Style style) const {
|
||||
// Cannot draw a NULL / empty string
|
||||
if (text == nullptr || *text == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
|
||||
// No printable characters
|
||||
if (!font.hasPrintableChars(text, style)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For 90° counter-clockwise rotation:
|
||||
// Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction
|
||||
// Text reads from top to bottom
|
||||
|
||||
const int advanceY = font.getData(style)->advanceY;
|
||||
const int ascender = font.getData(style)->ascender;
|
||||
|
||||
int yPos = y; // Current Y position (increases as we draw characters)
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
if (!glyph) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int is2Bit = font.getData(style)->is2Bit;
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
const int top = glyph->top;
|
||||
|
||||
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
for (int glyphX = 0; glyphX < width; glyphX++) {
|
||||
const int pixelPosition = glyphY * width + glyphX;
|
||||
|
||||
// 90° counter-clockwise rotation transformation:
|
||||
// screenX = mirrored CW X (right-to-left within advanceY span)
|
||||
// screenY = yPos + (left + glyphX) (downward)
|
||||
const int screenX = x + advanceY - 1 - (ascender - top + glyphY);
|
||||
const int screenY = yPos + left + glyphX;
|
||||
|
||||
if (is2Bit) {
|
||||
const uint8_t byte = bitmap[pixelPosition / 4];
|
||||
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
||||
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
||||
|
||||
if (renderMode == BW && bmpVal < 3) {
|
||||
drawPixel(screenX, screenY, black);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
}
|
||||
} else {
|
||||
const uint8_t byte = bitmap[pixelPosition / 8];
|
||||
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
||||
|
||||
if ((byte >> bit_index) & 1) {
|
||||
drawPixel(screenX, screenY, black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next character position (going down, so increase Y)
|
||||
yPos += glyph->advanceX;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||
|
||||
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||
@@ -872,8 +1042,7 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
// Check if any chunks are already allocated
|
||||
if (bwBufferChunks[i]) {
|
||||
Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n",
|
||||
millis(), i);
|
||||
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
||||
free(bwBufferChunks[i]);
|
||||
bwBufferChunks[i] = nullptr;
|
||||
}
|
||||
@@ -882,8 +1051,7 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
||||
|
||||
if (!bwBufferChunks[i]) {
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i,
|
||||
BW_BUFFER_CHUNK_SIZE);
|
||||
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, BW_BUFFER_CHUNK_SIZE);
|
||||
// Free previously allocated chunks
|
||||
freeBwBufferChunks();
|
||||
return false;
|
||||
@@ -892,8 +1060,7 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
||||
BW_BUFFER_CHUNK_SIZE);
|
||||
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -920,7 +1087,7 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
// Check if chunk is missing
|
||||
if (!bwBufferChunks[i]) {
|
||||
Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis());
|
||||
LOG_ERR("GFX", "!! BW buffer chunks not stored - this is likely a bug");
|
||||
freeBwBufferChunks();
|
||||
return;
|
||||
}
|
||||
@@ -932,7 +1099,7 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||
|
||||
freeBwBufferChunks();
|
||||
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
|
||||
LOG_DBG("GFX", "Restored and freed BW buffer chunks");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -954,7 +1121,7 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
|
||||
|
||||
// no glyph?
|
||||
if (!glyph) {
|
||||
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
||||
LOG_ERR("GFX", "No glyph for codepoint %d", cp);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,13 +70,15 @@ class GfxRenderer {
|
||||
int getScreenHeight() const;
|
||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||
// void displayWindow(int x, int y, int width, int height) const;
|
||||
void displayWindow(int x, int y, int width, int height,
|
||||
HalDisplay::RefreshMode mode = HalDisplay::FAST_REFRESH) const;
|
||||
void invertScreen() const;
|
||||
void clearScreen(uint8_t color = 0xFF) const;
|
||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||
|
||||
// Drawing
|
||||
void drawPixel(int x, int y, bool state = true) const;
|
||||
void drawPixelGray(int x, int y, uint8_t val2bit) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
|
||||
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
||||
@@ -110,13 +112,16 @@ class GfxRenderer {
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
|
||||
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
|
||||
// Helpers for drawing rotated text (for side buttons)
|
||||
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
void drawTextRotated90CCW(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getTextHeight(int fontId) const;
|
||||
|
||||
// Grayscale functions
|
||||
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
||||
RenderMode getRenderMode() const { return renderMode; }
|
||||
void copyGrayscaleLsbBuffers() const;
|
||||
void copyGrayscaleMsbBuffers() const;
|
||||
void displayGrayBuffer() const;
|
||||
|
||||
96
lib/I18n/I18n.cpp
Normal file
96
lib/I18n/I18n.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "I18n.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "I18nStrings.h"
|
||||
|
||||
using namespace i18n_strings;
|
||||
|
||||
// Settings file path
|
||||
static constexpr const char* SETTINGS_FILE = "/.crosspoint/language.bin";
|
||||
static constexpr uint8_t SETTINGS_VERSION = 1;
|
||||
|
||||
I18n& I18n::getInstance() {
|
||||
static I18n instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
const char* I18n::get(StrId id) const {
|
||||
const auto index = static_cast<size_t>(id);
|
||||
if (index >= static_cast<size_t>(StrId::_COUNT)) {
|
||||
return "???";
|
||||
}
|
||||
|
||||
// Use generated helper function - no hardcoded switch needed!
|
||||
const char* const* strings = getStringArray(_language);
|
||||
return strings[index];
|
||||
}
|
||||
|
||||
void I18n::setLanguage(Language lang) {
|
||||
if (lang >= Language::_COUNT) {
|
||||
return;
|
||||
}
|
||||
_language = lang;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
const char* I18n::getLanguageName(Language lang) const {
|
||||
const auto index = static_cast<size_t>(lang);
|
||||
if (index >= static_cast<size_t>(Language::_COUNT)) {
|
||||
return "???";
|
||||
}
|
||||
return LANGUAGE_NAMES[index];
|
||||
}
|
||||
|
||||
void I18n::saveSettings() {
|
||||
Storage.mkdir("/.crosspoint");
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("I18N", SETTINGS_FILE, file)) {
|
||||
Serial.printf("[I18N] Failed to save settings\n");
|
||||
return;
|
||||
}
|
||||
|
||||
serialization::writePod(file, SETTINGS_VERSION);
|
||||
serialization::writePod(file, static_cast<uint8_t>(_language));
|
||||
|
||||
file.close();
|
||||
Serial.printf("[I18N] Settings saved: language=%d\n", static_cast<int>(_language));
|
||||
}
|
||||
|
||||
void I18n::loadSettings() {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("I18N", SETTINGS_FILE, file)) {
|
||||
Serial.printf("[I18N] No settings file, using default (English)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(file, version);
|
||||
if (version != SETTINGS_VERSION) {
|
||||
Serial.printf("[I18N] Settings version mismatch\n");
|
||||
file.close();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t lang;
|
||||
serialization::readPod(file, lang);
|
||||
if (lang < static_cast<size_t>(Language::_COUNT)) {
|
||||
_language = static_cast<Language>(lang);
|
||||
Serial.printf("[I18N] Loaded language: %d\n", static_cast<int>(_language));
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Generate character set for a specific language
|
||||
const char* I18n::getCharacterSet(Language lang) {
|
||||
const auto langIndex = static_cast<size_t>(lang);
|
||||
if (langIndex >= static_cast<size_t>(Language::_COUNT)) {
|
||||
lang = Language::ENGLISH; // Fallback to first language
|
||||
}
|
||||
|
||||
return CHARACTER_SETS[static_cast<size_t>(lang)];
|
||||
}
|
||||
42
lib/I18n/I18n.h
Normal file
42
lib/I18n/I18n.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "I18nKeys.h"
|
||||
/**
|
||||
* Internationalization (i18n) system for CrossPoint Reader
|
||||
*/
|
||||
|
||||
class I18n {
|
||||
public:
|
||||
static I18n& getInstance();
|
||||
|
||||
// Disable copy
|
||||
I18n(const I18n&) = delete;
|
||||
I18n& operator=(const I18n&) = delete;
|
||||
|
||||
// Get localized string by ID
|
||||
const char* get(StrId id) const;
|
||||
|
||||
const char* operator[](StrId id) const { return get(id); }
|
||||
|
||||
Language getLanguage() const { return _language; }
|
||||
void setLanguage(Language lang);
|
||||
const char* getLanguageName(Language lang) const;
|
||||
|
||||
void saveSettings();
|
||||
void loadSettings();
|
||||
|
||||
// Get all unique characters used in a specific language
|
||||
// Returns a sorted string of unique characters
|
||||
static const char* getCharacterSet(Language lang);
|
||||
|
||||
private:
|
||||
I18n() : _language(Language::ENGLISH) {}
|
||||
|
||||
Language _language;
|
||||
};
|
||||
|
||||
// Convenience macros
|
||||
#define tr(id) I18n::getInstance().get(StrId::id)
|
||||
#define I18N I18n::getInstance()
|
||||
397
lib/I18n/I18nKeys.h
Normal file
397
lib/I18n/I18nKeys.h
Normal file
@@ -0,0 +1,397 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
|
||||
|
||||
// Forward declaration for string arrays
|
||||
namespace i18n_strings {
|
||||
extern const char* const STRINGS_EN[];
|
||||
extern const char* const STRINGS_ES[];
|
||||
extern const char* const STRINGS_FR[];
|
||||
extern const char* const STRINGS_DE[];
|
||||
extern const char* const STRINGS_CZ[];
|
||||
extern const char* const STRINGS_PO[];
|
||||
extern const char* const STRINGS_RU[];
|
||||
extern const char* const STRINGS_SV[];
|
||||
} // namespace i18n_strings
|
||||
|
||||
// Language enum
|
||||
enum class Language : uint8_t {
|
||||
ENGLISH = 0,
|
||||
SPANISH = 1,
|
||||
FRENCH = 2,
|
||||
GERMAN = 3,
|
||||
CZECH = 4,
|
||||
PORTUGUESE = 5,
|
||||
RUSSIAN = 6,
|
||||
SWEDISH = 7,
|
||||
_COUNT
|
||||
};
|
||||
|
||||
// Language display names (defined in I18nStrings.cpp)
|
||||
extern const char* const LANGUAGE_NAMES[];
|
||||
|
||||
// Character sets for each language (defined in I18nStrings.cpp)
|
||||
extern const char* const CHARACTER_SETS[];
|
||||
|
||||
// String IDs
|
||||
enum class StrId : uint16_t {
|
||||
STR_CROSSPOINT,
|
||||
STR_BOOTING,
|
||||
STR_SLEEPING,
|
||||
STR_ENTERING_SLEEP,
|
||||
STR_BROWSE_FILES,
|
||||
STR_FILE_TRANSFER,
|
||||
STR_SETTINGS_TITLE,
|
||||
STR_CALIBRE_LIBRARY,
|
||||
STR_CONTINUE_READING,
|
||||
STR_NO_OPEN_BOOK,
|
||||
STR_START_READING,
|
||||
STR_BOOKS,
|
||||
STR_NO_BOOKS_FOUND,
|
||||
STR_SELECT_CHAPTER,
|
||||
STR_NO_CHAPTERS,
|
||||
STR_END_OF_BOOK,
|
||||
STR_EMPTY_CHAPTER,
|
||||
STR_INDEXING,
|
||||
STR_MEMORY_ERROR,
|
||||
STR_PAGE_LOAD_ERROR,
|
||||
STR_EMPTY_FILE,
|
||||
STR_OUT_OF_BOUNDS,
|
||||
STR_LOADING,
|
||||
STR_LOAD_XTC_FAILED,
|
||||
STR_LOAD_TXT_FAILED,
|
||||
STR_LOAD_EPUB_FAILED,
|
||||
STR_SD_CARD_ERROR,
|
||||
STR_WIFI_NETWORKS,
|
||||
STR_NO_NETWORKS,
|
||||
STR_NETWORKS_FOUND,
|
||||
STR_SCANNING,
|
||||
STR_CONNECTING,
|
||||
STR_CONNECTED,
|
||||
STR_CONNECTION_FAILED,
|
||||
STR_CONNECTION_TIMEOUT,
|
||||
STR_FORGET_NETWORK,
|
||||
STR_SAVE_PASSWORD,
|
||||
STR_REMOVE_PASSWORD,
|
||||
STR_PRESS_OK_SCAN,
|
||||
STR_PRESS_ANY_CONTINUE,
|
||||
STR_SELECT_HINT,
|
||||
STR_HOW_CONNECT,
|
||||
STR_JOIN_NETWORK,
|
||||
STR_CREATE_HOTSPOT,
|
||||
STR_JOIN_DESC,
|
||||
STR_HOTSPOT_DESC,
|
||||
STR_STARTING_HOTSPOT,
|
||||
STR_HOTSPOT_MODE,
|
||||
STR_CONNECT_WIFI_HINT,
|
||||
STR_OPEN_URL_HINT,
|
||||
STR_OR_HTTP_PREFIX,
|
||||
STR_SCAN_QR_HINT,
|
||||
STR_CALIBRE_WIRELESS,
|
||||
STR_CALIBRE_WEB_URL,
|
||||
STR_CONNECT_WIRELESS,
|
||||
STR_NETWORK_LEGEND,
|
||||
STR_MAC_ADDRESS,
|
||||
STR_CHECKING_WIFI,
|
||||
STR_ENTER_WIFI_PASSWORD,
|
||||
STR_ENTER_TEXT,
|
||||
STR_TO_PREFIX,
|
||||
STR_CALIBRE_DISCOVERING,
|
||||
STR_CALIBRE_CONNECTING_TO,
|
||||
STR_CALIBRE_CONNECTED_TO,
|
||||
STR_CALIBRE_WAITING_COMMANDS,
|
||||
STR_CONNECTION_FAILED_RETRYING,
|
||||
STR_CALIBRE_DISCONNECTED,
|
||||
STR_CALIBRE_WAITING_TRANSFER,
|
||||
STR_CALIBRE_TRANSFER_HINT,
|
||||
STR_CALIBRE_RECEIVING,
|
||||
STR_CALIBRE_RECEIVED,
|
||||
STR_CALIBRE_WAITING_MORE,
|
||||
STR_CALIBRE_FAILED_CREATE_FILE,
|
||||
STR_CALIBRE_PASSWORD_REQUIRED,
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED,
|
||||
STR_CALIBRE_INSTRUCTION_1,
|
||||
STR_CALIBRE_INSTRUCTION_2,
|
||||
STR_CALIBRE_INSTRUCTION_3,
|
||||
STR_CALIBRE_INSTRUCTION_4,
|
||||
STR_CAT_DISPLAY,
|
||||
STR_CAT_READER,
|
||||
STR_CAT_CONTROLS,
|
||||
STR_CAT_SYSTEM,
|
||||
STR_SLEEP_SCREEN,
|
||||
STR_SLEEP_COVER_MODE,
|
||||
STR_STATUS_BAR,
|
||||
STR_HIDE_BATTERY,
|
||||
STR_EXTRA_SPACING,
|
||||
STR_TEXT_AA,
|
||||
STR_SHORT_PWR_BTN,
|
||||
STR_ORIENTATION,
|
||||
STR_FRONT_BTN_LAYOUT,
|
||||
STR_SIDE_BTN_LAYOUT,
|
||||
STR_LONG_PRESS_SKIP,
|
||||
STR_FONT_FAMILY,
|
||||
STR_EXT_READER_FONT,
|
||||
STR_EXT_CHINESE_FONT,
|
||||
STR_EXT_UI_FONT,
|
||||
STR_FONT_SIZE,
|
||||
STR_LINE_SPACING,
|
||||
STR_ASCII_LETTER_SPACING,
|
||||
STR_ASCII_DIGIT_SPACING,
|
||||
STR_CJK_SPACING,
|
||||
STR_COLOR_MODE,
|
||||
STR_SCREEN_MARGIN,
|
||||
STR_PARA_ALIGNMENT,
|
||||
STR_HYPHENATION,
|
||||
STR_TIME_TO_SLEEP,
|
||||
STR_REFRESH_FREQ,
|
||||
STR_CALIBRE_SETTINGS,
|
||||
STR_KOREADER_SYNC,
|
||||
STR_CHECK_UPDATES,
|
||||
STR_LANGUAGE,
|
||||
STR_SELECT_WALLPAPER,
|
||||
STR_CLEAR_READING_CACHE,
|
||||
STR_CALIBRE,
|
||||
STR_USERNAME,
|
||||
STR_PASSWORD,
|
||||
STR_SYNC_SERVER_URL,
|
||||
STR_DOCUMENT_MATCHING,
|
||||
STR_AUTHENTICATE,
|
||||
STR_KOREADER_USERNAME,
|
||||
STR_KOREADER_PASSWORD,
|
||||
STR_FILENAME,
|
||||
STR_BINARY,
|
||||
STR_SET_CREDENTIALS_FIRST,
|
||||
STR_WIFI_CONN_FAILED,
|
||||
STR_AUTHENTICATING,
|
||||
STR_AUTH_SUCCESS,
|
||||
STR_KOREADER_AUTH,
|
||||
STR_SYNC_READY,
|
||||
STR_AUTH_FAILED,
|
||||
STR_DONE,
|
||||
STR_CLEAR_CACHE_WARNING_1,
|
||||
STR_CLEAR_CACHE_WARNING_2,
|
||||
STR_CLEAR_CACHE_WARNING_3,
|
||||
STR_CLEAR_CACHE_WARNING_4,
|
||||
STR_CLEARING_CACHE,
|
||||
STR_CACHE_CLEARED,
|
||||
STR_ITEMS_REMOVED,
|
||||
STR_FAILED_LOWER,
|
||||
STR_CLEAR_CACHE_FAILED,
|
||||
STR_CHECK_SERIAL_OUTPUT,
|
||||
STR_DARK,
|
||||
STR_LIGHT,
|
||||
STR_CUSTOM,
|
||||
STR_COVER,
|
||||
STR_NONE_OPT,
|
||||
STR_FIT,
|
||||
STR_CROP,
|
||||
STR_NO_PROGRESS,
|
||||
STR_FULL_OPT,
|
||||
STR_NEVER,
|
||||
STR_IN_READER,
|
||||
STR_ALWAYS,
|
||||
STR_IGNORE,
|
||||
STR_SLEEP,
|
||||
STR_PAGE_TURN,
|
||||
STR_PORTRAIT,
|
||||
STR_LANDSCAPE_CW,
|
||||
STR_INVERTED,
|
||||
STR_LANDSCAPE_CCW,
|
||||
STR_FRONT_LAYOUT_BCLR,
|
||||
STR_FRONT_LAYOUT_LRBC,
|
||||
STR_FRONT_LAYOUT_LBCR,
|
||||
STR_PREV_NEXT,
|
||||
STR_NEXT_PREV,
|
||||
STR_BOOKERLY,
|
||||
STR_NOTO_SANS,
|
||||
STR_OPEN_DYSLEXIC,
|
||||
STR_SMALL,
|
||||
STR_MEDIUM,
|
||||
STR_LARGE,
|
||||
STR_X_LARGE,
|
||||
STR_TIGHT,
|
||||
STR_NORMAL,
|
||||
STR_WIDE,
|
||||
STR_JUSTIFY,
|
||||
STR_ALIGN_LEFT,
|
||||
STR_CENTER,
|
||||
STR_ALIGN_RIGHT,
|
||||
STR_MIN_1,
|
||||
STR_MIN_5,
|
||||
STR_MIN_10,
|
||||
STR_MIN_15,
|
||||
STR_MIN_30,
|
||||
STR_PAGES_1,
|
||||
STR_PAGES_5,
|
||||
STR_PAGES_10,
|
||||
STR_PAGES_15,
|
||||
STR_PAGES_30,
|
||||
STR_UPDATE,
|
||||
STR_CHECKING_UPDATE,
|
||||
STR_NEW_UPDATE,
|
||||
STR_CURRENT_VERSION,
|
||||
STR_NEW_VERSION,
|
||||
STR_UPDATING,
|
||||
STR_NO_UPDATE,
|
||||
STR_UPDATE_FAILED,
|
||||
STR_UPDATE_COMPLETE,
|
||||
STR_POWER_ON_HINT,
|
||||
STR_EXTERNAL_FONT,
|
||||
STR_BUILTIN_DISABLED,
|
||||
STR_NO_ENTRIES,
|
||||
STR_DOWNLOADING,
|
||||
STR_DOWNLOAD_FAILED,
|
||||
STR_ERROR_MSG,
|
||||
STR_UNNAMED,
|
||||
STR_NO_SERVER_URL,
|
||||
STR_FETCH_FEED_FAILED,
|
||||
STR_PARSE_FEED_FAILED,
|
||||
STR_NETWORK_PREFIX,
|
||||
STR_IP_ADDRESS_PREFIX,
|
||||
STR_SCAN_QR_WIFI_HINT,
|
||||
STR_ERROR_GENERAL_FAILURE,
|
||||
STR_ERROR_NETWORK_NOT_FOUND,
|
||||
STR_ERROR_CONNECTION_TIMEOUT,
|
||||
STR_SD_CARD,
|
||||
STR_BACK,
|
||||
STR_EXIT,
|
||||
STR_HOME,
|
||||
STR_SAVE,
|
||||
STR_SELECT,
|
||||
STR_TOGGLE,
|
||||
STR_CONFIRM,
|
||||
STR_CANCEL,
|
||||
STR_CONNECT,
|
||||
STR_OPEN,
|
||||
STR_DOWNLOAD,
|
||||
STR_RETRY,
|
||||
STR_YES,
|
||||
STR_NO,
|
||||
STR_STATE_ON,
|
||||
STR_STATE_OFF,
|
||||
STR_SET,
|
||||
STR_NOT_SET,
|
||||
STR_DIR_LEFT,
|
||||
STR_DIR_RIGHT,
|
||||
STR_DIR_UP,
|
||||
STR_DIR_DOWN,
|
||||
STR_CAPS_ON,
|
||||
STR_CAPS_OFF,
|
||||
STR_OK_BUTTON,
|
||||
STR_ON_MARKER,
|
||||
STR_SLEEP_COVER_FILTER,
|
||||
STR_FILTER_CONTRAST,
|
||||
STR_STATUS_BAR_FULL_PERCENT,
|
||||
STR_STATUS_BAR_FULL_BOOK,
|
||||
STR_STATUS_BAR_BOOK_ONLY,
|
||||
STR_STATUS_BAR_FULL_CHAPTER,
|
||||
STR_UI_THEME,
|
||||
STR_THEME_CLASSIC,
|
||||
STR_THEME_LYRA,
|
||||
STR_SUNLIGHT_FADING_FIX,
|
||||
STR_REMAP_FRONT_BUTTONS,
|
||||
STR_OPDS_BROWSER,
|
||||
STR_COVER_CUSTOM,
|
||||
STR_RECENTS,
|
||||
STR_MENU_RECENT_BOOKS,
|
||||
STR_NO_RECENT_BOOKS,
|
||||
STR_CALIBRE_DESC,
|
||||
STR_FORGET_AND_REMOVE,
|
||||
STR_FORGET_BUTTON,
|
||||
STR_CALIBRE_STARTING,
|
||||
STR_CALIBRE_SETUP,
|
||||
STR_CALIBRE_STATUS,
|
||||
STR_CLEAR_BUTTON,
|
||||
STR_DEFAULT_VALUE,
|
||||
STR_REMAP_PROMPT,
|
||||
STR_UNASSIGNED,
|
||||
STR_ALREADY_ASSIGNED,
|
||||
STR_REMAP_RESET_HINT,
|
||||
STR_REMAP_CANCEL_HINT,
|
||||
STR_HW_BACK_LABEL,
|
||||
STR_HW_CONFIRM_LABEL,
|
||||
STR_HW_LEFT_LABEL,
|
||||
STR_HW_RIGHT_LABEL,
|
||||
STR_GO_TO_PERCENT,
|
||||
STR_GO_HOME_BUTTON,
|
||||
STR_SYNC_PROGRESS,
|
||||
STR_DELETE_CACHE,
|
||||
STR_CHAPTER_PREFIX,
|
||||
STR_PAGES_SEPARATOR,
|
||||
STR_BOOK_PREFIX,
|
||||
STR_KBD_SHIFT,
|
||||
STR_KBD_SHIFT_CAPS,
|
||||
STR_KBD_LOCK,
|
||||
STR_CALIBRE_URL_HINT,
|
||||
STR_PERCENT_STEP_HINT,
|
||||
STR_SYNCING_TIME,
|
||||
STR_CALC_HASH,
|
||||
STR_HASH_FAILED,
|
||||
STR_FETCH_PROGRESS,
|
||||
STR_UPLOAD_PROGRESS,
|
||||
STR_NO_CREDENTIALS_MSG,
|
||||
STR_KOREADER_SETUP_HINT,
|
||||
STR_PROGRESS_FOUND,
|
||||
STR_REMOTE_LABEL,
|
||||
STR_LOCAL_LABEL,
|
||||
STR_PAGE_OVERALL_FORMAT,
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT,
|
||||
STR_DEVICE_FROM_FORMAT,
|
||||
STR_APPLY_REMOTE,
|
||||
STR_UPLOAD_LOCAL,
|
||||
STR_NO_REMOTE_MSG,
|
||||
STR_UPLOAD_PROMPT,
|
||||
STR_UPLOAD_SUCCESS,
|
||||
STR_SYNC_FAILED_MSG,
|
||||
STR_SECTION_PREFIX,
|
||||
STR_UPLOAD,
|
||||
STR_BOOK_S_STYLE,
|
||||
STR_EMBEDDED_STYLE,
|
||||
STR_OPDS_SERVER_URL,
|
||||
STR_LETTERBOX_FILL,
|
||||
STR_DITHERED,
|
||||
STR_SOLID,
|
||||
STR_ADD_BOOKMARK,
|
||||
STR_REMOVE_BOOKMARK,
|
||||
STR_LOOKUP_WORD,
|
||||
STR_LOOKUP_HISTORY,
|
||||
STR_GO_TO_BOOKMARK,
|
||||
STR_CLOSE_BOOK,
|
||||
STR_DELETE_DICT_CACHE,
|
||||
STR_DEFAULT_OPTION,
|
||||
STR_BOOKMARK_ADDED,
|
||||
STR_BOOKMARK_REMOVED,
|
||||
STR_DICT_CACHE_DELETED,
|
||||
STR_NO_CACHE_TO_DELETE,
|
||||
STR_TABLE_OF_CONTENTS,
|
||||
// Sentinel - must be last
|
||||
_COUNT
|
||||
};
|
||||
|
||||
// Helper function to get string array for a language
|
||||
inline const char* const* getStringArray(Language lang) {
|
||||
switch (lang) {
|
||||
case Language::ENGLISH:
|
||||
return i18n_strings::STRINGS_EN;
|
||||
case Language::SPANISH:
|
||||
return i18n_strings::STRINGS_ES;
|
||||
case Language::FRENCH:
|
||||
return i18n_strings::STRINGS_FR;
|
||||
case Language::GERMAN:
|
||||
return i18n_strings::STRINGS_DE;
|
||||
case Language::CZECH:
|
||||
return i18n_strings::STRINGS_CZ;
|
||||
case Language::PORTUGUESE:
|
||||
return i18n_strings::STRINGS_PO;
|
||||
case Language::RUSSIAN:
|
||||
return i18n_strings::STRINGS_RU;
|
||||
case Language::SWEDISH:
|
||||
return i18n_strings::STRINGS_SV;
|
||||
default:
|
||||
return i18n_strings::STRINGS_EN;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get language count
|
||||
constexpr uint8_t getLanguageCount() { return static_cast<uint8_t>(Language::_COUNT); }
|
||||
19
lib/I18n/I18nStrings.h
Normal file
19
lib/I18n/I18nStrings.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
#include "I18nKeys.h"
|
||||
|
||||
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
|
||||
|
||||
namespace i18n_strings {
|
||||
|
||||
extern const char* const STRINGS_EN[];
|
||||
extern const char* const STRINGS_ES[];
|
||||
extern const char* const STRINGS_FR[];
|
||||
extern const char* const STRINGS_DE[];
|
||||
extern const char* const STRINGS_CZ[];
|
||||
extern const char* const STRINGS_PO[];
|
||||
extern const char* const STRINGS_RU[];
|
||||
extern const char* const STRINGS_SV[];
|
||||
|
||||
} // namespace i18n_strings
|
||||
317
lib/I18n/translations/czech.yaml
Normal file
317
lib/I18n/translations/czech.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Čeština"
|
||||
_language_code: "CZECH"
|
||||
_order: "4"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "SPUŠTĚNÍ"
|
||||
STR_SLEEPING: "SPÁNEK"
|
||||
STR_ENTERING_SLEEP: "Vstup do režimu spánku..."
|
||||
STR_BROWSE_FILES: "Procházet soubory"
|
||||
STR_FILE_TRANSFER: "Přenos souborů"
|
||||
STR_SETTINGS_TITLE: "Nastavení"
|
||||
STR_CALIBRE_LIBRARY: "Knihovna Calibre"
|
||||
STR_CONTINUE_READING: "Pokračovat ve čtení"
|
||||
STR_NO_OPEN_BOOK: "Žádná otevřená kniha"
|
||||
STR_START_READING: "Začněte číst níže"
|
||||
STR_BOOKS: "Knihy"
|
||||
STR_NO_BOOKS_FOUND: "Žádné knihy nenalezeny"
|
||||
STR_SELECT_CHAPTER: "Vybrat kapitolu"
|
||||
STR_NO_CHAPTERS: "Žádné kapitoly"
|
||||
STR_END_OF_BOOK: "Konec knihy"
|
||||
STR_EMPTY_CHAPTER: "Prázdná kapitola"
|
||||
STR_INDEXING: "Indexování..."
|
||||
STR_MEMORY_ERROR: "Chyba paměti"
|
||||
STR_PAGE_LOAD_ERROR: "Chyba načítání stránky"
|
||||
STR_EMPTY_FILE: "Prázdný soubor"
|
||||
STR_OUT_OF_BOUNDS: "Mimo hranice"
|
||||
STR_LOADING: "Načítání..."
|
||||
STR_LOAD_XTC_FAILED: "Nepodařilo se načíst XTC"
|
||||
STR_LOAD_TXT_FAILED: "Nepodařilo se načíst TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Nepodařilo se načíst EPUB"
|
||||
STR_SD_CARD_ERROR: "Chyba SD karty"
|
||||
STR_WIFI_NETWORKS: "Wi-Fi sítě"
|
||||
STR_NO_NETWORKS: "Žádné sítě nenalezeny"
|
||||
STR_NETWORKS_FOUND: "Nalezeno %zu sítí"
|
||||
STR_SCANNING: "Skenování..."
|
||||
STR_CONNECTING: "Připojování..."
|
||||
STR_CONNECTED: "Připojeno!"
|
||||
STR_CONNECTION_FAILED: "Připojení se nezdařilo"
|
||||
STR_CONNECTION_TIMEOUT: "Časový limit připojení"
|
||||
STR_FORGET_NETWORK: "Zapomenout síť?"
|
||||
STR_SAVE_PASSWORD: "Uložit heslo pro příště?"
|
||||
STR_REMOVE_PASSWORD: "Odstranit uložené heslo?"
|
||||
STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování"
|
||||
STR_PRESS_ANY_CONTINUE: "Pokračujte stiskem libovolné klávesy"
|
||||
STR_SELECT_HINT: "VLEVO/VPRAVO: Vybrat | OK: Potvrdit"
|
||||
STR_HOW_CONNECT: "Jak se chcete připojit?"
|
||||
STR_JOIN_NETWORK: "Připojit se k síti"
|
||||
STR_CREATE_HOTSPOT: "Vytvořit hotspot"
|
||||
STR_JOIN_DESC: "Připojit se k existující síti WiFi"
|
||||
STR_HOTSPOT_DESC: "Vytvořit síť WiFi, ke které se mohou připojit ostatní"
|
||||
STR_STARTING_HOTSPOT: "Spouštění hotspotu..."
|
||||
STR_HOTSPOT_MODE: "Režim hotspotu"
|
||||
STR_CONNECT_WIFI_HINT: "Připojte své zařízení k této síti WiFi"
|
||||
STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
|
||||
STR_OR_HTTP_PREFIX: "nebo http://"
|
||||
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "URL webu Calibre"
|
||||
STR_CONNECT_WIRELESS: "Připojit jako bezdrátové zařízení"
|
||||
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
||||
STR_MAC_ADDRESS: "MAC adresa:"
|
||||
STR_CHECKING_WIFI: "Kontrola WiFi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi"
|
||||
STR_ENTER_TEXT: "Zadejte text"
|
||||
STR_TO_PREFIX: "pro"
|
||||
STR_CALIBRE_DISCOVERING: "Prozkoumávání Calibre..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Připojování k"
|
||||
STR_CALIBRE_CONNECTED_TO: "Připojeno k"
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Čekám na příkazy…"
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Připojení se nezdařilo, opakování pokusu)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre odpojeno"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Čekání na přenos..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "Nezdaří-li se přenos, povolte\\n„Ignorovat volné místo“ v Calibre\\nnastavení pluginu SmartDevice."
|
||||
STR_CALIBRE_RECEIVING: "Příjem:"
|
||||
STR_CALIBRE_RECEIVED: "Přijato:"
|
||||
STR_CALIBRE_WAITING_MORE: "Čekání na další..."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Nepodařilo se vytvořit soubor"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Vyžadováno heslo"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Přenos přerušen"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
|
||||
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
|
||||
STR_CAT_DISPLAY: "Displej"
|
||||
STR_CAT_READER: "Čtečka"
|
||||
STR_CAT_CONTROLS: "Ovládací prvky"
|
||||
STR_CAT_SYSTEM: "Systém"
|
||||
STR_SLEEP_SCREEN: "Obrazovka spánku"
|
||||
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
|
||||
STR_STATUS_BAR: "Stavový řádek"
|
||||
STR_HIDE_BATTERY: "Skrýt baterii %"
|
||||
STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
|
||||
STR_TEXT_AA: "Vyhlazování textu"
|
||||
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
|
||||
STR_ORIENTATION: "Orientace čtení"
|
||||
STR_FRONT_BTN_LAYOUT: "Rozvržení předních tlačítek"
|
||||
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
|
||||
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
|
||||
STR_FONT_FAMILY: "Rodina písem čtečky"
|
||||
STR_EXT_READER_FONT: "Písmo externí čtečky"
|
||||
STR_EXT_CHINESE_FONT: "Písmo čtečky"
|
||||
STR_EXT_UI_FONT: "Písmo rozhraní"
|
||||
STR_FONT_SIZE: "Velikost písma rozhraní"
|
||||
STR_LINE_SPACING: "Řádkování čtečky"
|
||||
STR_ASCII_LETTER_SPACING: "Mezery písmen ASCII"
|
||||
STR_ASCII_DIGIT_SPACING: "Mezery číslic ASCII"
|
||||
STR_CJK_SPACING: "Mezery CJK"
|
||||
STR_COLOR_MODE: "Režim barev"
|
||||
STR_SCREEN_MARGIN: "Okraj obrazovky čtečky"
|
||||
STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky"
|
||||
STR_HYPHENATION: "Dělení slov"
|
||||
STR_TIME_TO_SLEEP: "Čas do uspání"
|
||||
STR_REFRESH_FREQ: "Frekvence obnovení"
|
||||
STR_CALIBRE_SETTINGS: "Nastavení Calibre"
|
||||
STR_KOREADER_SYNC: "KOReaderu Sync"
|
||||
STR_CHECK_UPDATES: "Zkontrolovat aktualizace"
|
||||
STR_LANGUAGE: "Jazyk"
|
||||
STR_SELECT_WALLPAPER: "Vybrat tapetu"
|
||||
STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Uživatelské jméno"
|
||||
STR_PASSWORD: "Heslo"
|
||||
STR_SYNC_SERVER_URL: "URL synch. serveru"
|
||||
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
||||
STR_AUTHENTICATE: "Ověření"
|
||||
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
||||
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
||||
STR_FILENAME: "Název souboru"
|
||||
STR_BINARY: "Binární"
|
||||
STR_SET_CREDENTIALS_FIRST: "Nastavte přihlašovací údaje"
|
||||
STR_WIFI_CONN_FAILED: "Připojení k Wi-Fi selhalo"
|
||||
STR_AUTHENTICATING: "Ověřování..."
|
||||
STR_AUTH_SUCCESS: "Úspěšné ověření!"
|
||||
STR_KOREADER_AUTH: "Ověření KOReaderu"
|
||||
STR_SYNC_READY: "Synchronizace KOReaderu je připravena k použití"
|
||||
STR_AUTH_FAILED: "Ověření selhalo"
|
||||
STR_DONE: "Hotovo"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Tímto vymažete všechna data knih v mezipaměti."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Veškerý průběh čtení bude ztracen!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Knihy bude nutné znovu indexovat"
|
||||
STR_CLEAR_CACHE_WARNING_4: "při opětovném otevření."
|
||||
STR_CLEARING_CACHE: "Mazání mezipaměti..."
|
||||
STR_CACHE_CLEARED: "Mezipaměť vymazána"
|
||||
STR_ITEMS_REMOVED: "položky odstraněny"
|
||||
STR_FAILED_LOWER: "selhalo"
|
||||
STR_CLEAR_CACHE_FAILED: "Vymazání mezipaměti se nezdařilo"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti naleznete v sériovém výstupu"
|
||||
STR_DARK: "Tmavý"
|
||||
STR_LIGHT: "Světlý"
|
||||
STR_CUSTOM: "Vlastní"
|
||||
STR_COVER: "Obálka"
|
||||
STR_NONE_OPT: "Žádný"
|
||||
STR_FIT: "Přizpůsobit"
|
||||
STR_CROP: "Oříznout"
|
||||
STR_NO_PROGRESS: "Žádný postup"
|
||||
STR_FULL_OPT: "Plná"
|
||||
STR_NEVER: "Nikdy"
|
||||
STR_IN_READER: "Ve čtečce"
|
||||
STR_ALWAYS: "Vždy"
|
||||
STR_IGNORE: "Ignorovat"
|
||||
STR_SLEEP: "Spánek"
|
||||
STR_PAGE_TURN: "Otáčení stránek"
|
||||
STR_PORTRAIT: "Na výšku"
|
||||
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
||||
STR_INVERTED: "Invertovaný"
|
||||
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
||||
STR_FRONT_LAYOUT_BCLR: "Zpět, Potvrdit, Vlevo, Vpravo"
|
||||
STR_FRONT_LAYOUT_LRBC: "Vlevo, Vpravo, Zpět, Potvrdit"
|
||||
STR_FRONT_LAYOUT_LBCR: "Vlevo, Zpět, Potvrdit, Vpravo"
|
||||
STR_PREV_NEXT: "Předchozí/Další"
|
||||
STR_NEXT_PREV: "Další/Předchozí"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Malý"
|
||||
STR_MEDIUM: "Střední"
|
||||
STR_LARGE: "Velký"
|
||||
STR_X_LARGE: "Obří"
|
||||
STR_TIGHT: "Těsný"
|
||||
STR_NORMAL: "Normální"
|
||||
STR_WIDE: "Široký"
|
||||
STR_JUSTIFY: "Zarovnat do bloku"
|
||||
STR_ALIGN_LEFT: "Vlevo"
|
||||
STR_CENTER: "Na střed"
|
||||
STR_ALIGN_RIGHT: "Vpravo"
|
||||
STR_MIN_1: "1 min"
|
||||
STR_MIN_5: "5 min"
|
||||
STR_MIN_10: "10 min"
|
||||
STR_MIN_15: "15 min"
|
||||
STR_MIN_30: "30 min"
|
||||
STR_PAGES_1: "1 stránka"
|
||||
STR_PAGES_5: "5 stránek"
|
||||
STR_PAGES_10: "10 stránek"
|
||||
STR_PAGES_15: "15 stránek"
|
||||
STR_PAGES_30: "30 stránek"
|
||||
STR_UPDATE: "Aktualizace"
|
||||
STR_CHECKING_UPDATE: "Kontrola aktualizací…"
|
||||
STR_NEW_UPDATE: "Nová aktualizace k dispozici!"
|
||||
STR_CURRENT_VERSION: "Aktuální verze:"
|
||||
STR_NEW_VERSION: "Nová verze:"
|
||||
STR_UPDATING: "Aktualizace..."
|
||||
STR_NO_UPDATE: "Žádná aktualizace k dispozici"
|
||||
STR_UPDATE_FAILED: "Aktualizace selhala"
|
||||
STR_UPDATE_COMPLETE: "Aktualizace dokončena"
|
||||
STR_POWER_ON_HINT: "Stiskněte a podržte tlačítko napájení pro opětovné zapnutí"
|
||||
STR_EXTERNAL_FONT: "Externí písmo"
|
||||
STR_BUILTIN_DISABLED: "Vestavěné (Zakázáno)"
|
||||
STR_NO_ENTRIES: "Žádné položky nenalezeny"
|
||||
STR_DOWNLOADING: "Stahování..."
|
||||
STR_DOWNLOAD_FAILED: "Stahování selhalo"
|
||||
STR_ERROR_MSG: "Chyba:"
|
||||
STR_UNNAMED: "Nepojmenované"
|
||||
STR_NO_SERVER_URL: "Není nakonfigurována adresa URL serveru"
|
||||
STR_FETCH_FEED_FAILED: "Načtení kanálu se nezdařilo"
|
||||
STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo"
|
||||
STR_NETWORK_PREFIX: "Síť:"
|
||||
STR_IP_ADDRESS_PREFIX: "IP adresa:"
|
||||
STR_SCAN_QR_WIFI_HINT: "nebo naskenujte QR kód telefonem pro připojení k Wi-Fi."
|
||||
STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení"
|
||||
STR_SD_CARD: "SD karta"
|
||||
STR_BACK: "« Zpět"
|
||||
STR_EXIT: "« Konec"
|
||||
STR_HOME: "« Domů"
|
||||
STR_SAVE: "« Uložit"
|
||||
STR_SELECT: "Vybrat"
|
||||
STR_TOGGLE: "Přepnout"
|
||||
STR_CONFIRM: "Potvrdit"
|
||||
STR_CANCEL: "Zrušit"
|
||||
STR_CONNECT: "Připojit"
|
||||
STR_OPEN: "Otevřít"
|
||||
STR_DOWNLOAD: "Stáhnout"
|
||||
STR_RETRY: "Zkusit znovu"
|
||||
STR_YES: "Ano"
|
||||
STR_NO: "Ne"
|
||||
STR_STATE_ON: "ZAP"
|
||||
STR_STATE_OFF: "VYP"
|
||||
STR_SET: "Nastavit"
|
||||
STR_NOT_SET: "Nenastaveno"
|
||||
STR_DIR_LEFT: "Vlevo"
|
||||
STR_DIR_RIGHT: "Vpravo"
|
||||
STR_DIR_UP: "Nahoru"
|
||||
STR_DIR_DOWN: "Dolů"
|
||||
STR_CAPS_ON: "PÍSMO"
|
||||
STR_CAPS_OFF: "písmo"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[ZAP]"
|
||||
STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Plný s procenty"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Plný s pruhem knih"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Pouze pruh knih"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol"
|
||||
STR_UI_THEME: "Šablona rozhraní"
|
||||
STR_THEME_CLASSIC: "Klasická"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
|
||||
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
|
||||
STR_OPDS_BROWSER: "Prohlížeč OPDS"
|
||||
STR_COVER_CUSTOM: "Obálka + Vlastní"
|
||||
STR_RECENTS: "Nedávné"
|
||||
STR_MENU_RECENT_BOOKS: "Nedávné knihy"
|
||||
STR_NO_RECENT_BOOKS: "Žádné nedávné knihy"
|
||||
STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Zapomenout síť a odstranit uložené heslo?"
|
||||
STR_FORGET_BUTTON: "Zapomenout na síť"
|
||||
STR_CALIBRE_STARTING: "Spuštění Calibre..."
|
||||
STR_CALIBRE_SETUP: "Nastavení"
|
||||
STR_CALIBRE_STATUS: "Stav"
|
||||
STR_CLEAR_BUTTON: "Vymazat"
|
||||
STR_DEFAULT_VALUE: "Výchozí"
|
||||
STR_REMAP_PROMPT: "Stiskněte přední tlačítko pro každou roli"
|
||||
STR_UNASSIGNED: "Nepřiřazeno"
|
||||
STR_ALREADY_ASSIGNED: "Již přiřazeno"
|
||||
STR_REMAP_RESET_HINT: "Boční tlačítko Nahoru: Obnovit výchozí rozvržení"
|
||||
STR_REMAP_CANCEL_HINT: "Boční tlačítko Dolů: Zrušit přemapování"
|
||||
STR_HW_BACK_LABEL: "Zpět (1. tlačítko)"
|
||||
STR_HW_CONFIRM_LABEL: "Potvrdit (2. tlačítko)"
|
||||
STR_HW_LEFT_LABEL: "Vlevo (3. tlačítko)"
|
||||
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačítko)"
|
||||
STR_GO_TO_PERCENT: "Přejít na %"
|
||||
STR_GO_HOME_BUTTON: "Přejít Domů"
|
||||
STR_SYNC_PROGRESS: "Průběh synchronizace"
|
||||
STR_DELETE_CACHE: "Smazat mezipaměť knihy"
|
||||
STR_CHAPTER_PREFIX: "Kapitola:"
|
||||
STR_PAGES_SEPARATOR: "stránek |"
|
||||
STR_BOOK_PREFIX: "Kniha:"
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "ZÁMEK"
|
||||
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
|
||||
STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %"
|
||||
STR_SYNCING_TIME: "Čas synchronizace..."
|
||||
STR_CALC_HASH: "Výpočet hashe dokumentu..."
|
||||
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
|
||||
STR_FETCH_PROGRESS: "Načítání vzdáleného průběhu..."
|
||||
STR_UPLOAD_PROGRESS: "Průběh nahrávání..."
|
||||
STR_NO_CREDENTIALS_MSG: "Přihlašovací údaje nejsou nakonfigurovány"
|
||||
STR_KOREADER_SETUP_HINT: "Nastavit účet KOReader v Nastavení"
|
||||
STR_PROGRESS_FOUND: "Nalezen průběh!"
|
||||
STR_REMOTE_LABEL: "Vzdálené:"
|
||||
STR_LOCAL_LABEL: "Lokální:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Stránka %d, celkově %.2f%%"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stránka %d/%d, celkově %.2f%%"
|
||||
STR_DEVICE_FROM_FORMAT: " Od: %s"
|
||||
STR_APPLY_REMOTE: "Použít vzdálený postup"
|
||||
STR_UPLOAD_LOCAL: "Nahrát lokální postup"
|
||||
STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený postup"
|
||||
STR_UPLOAD_PROMPT: "Nahrát aktuální pozici?"
|
||||
STR_UPLOAD_SUCCESS: "Postup nahrán!"
|
||||
STR_SYNC_FAILED_MSG: "Synchronizace se nezdařila"
|
||||
STR_SECTION_PREFIX: "Sekce"
|
||||
STR_UPLOAD: "Nahrát"
|
||||
STR_BOOK_S_STYLE: "Styl knihy"
|
||||
STR_EMBEDDED_STYLE: "Vložený styl"
|
||||
STR_OPDS_SERVER_URL: "URL serveru OPDS"
|
||||
333
lib/I18n/translations/english.yaml
Normal file
333
lib/I18n/translations/english.yaml
Normal file
@@ -0,0 +1,333 @@
|
||||
_language_name: "English"
|
||||
_language_code: "ENGLISH"
|
||||
_order: "0"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "BOOTING"
|
||||
STR_SLEEPING: "SLEEPING"
|
||||
STR_ENTERING_SLEEP: "Entering Sleep..."
|
||||
STR_BROWSE_FILES: "Browse Files"
|
||||
STR_FILE_TRANSFER: "File Transfer"
|
||||
STR_SETTINGS_TITLE: "Settings"
|
||||
STR_CALIBRE_LIBRARY: "Calibre Library"
|
||||
STR_CONTINUE_READING: "Continue Reading"
|
||||
STR_NO_OPEN_BOOK: "No open book"
|
||||
STR_START_READING: "Start reading below"
|
||||
STR_BOOKS: "Books"
|
||||
STR_NO_BOOKS_FOUND: "No books found"
|
||||
STR_SELECT_CHAPTER: "Select Chapter"
|
||||
STR_NO_CHAPTERS: "No chapters"
|
||||
STR_END_OF_BOOK: "End of book"
|
||||
STR_EMPTY_CHAPTER: "Empty chapter"
|
||||
STR_INDEXING: "Indexing..."
|
||||
STR_MEMORY_ERROR: "Memory error"
|
||||
STR_PAGE_LOAD_ERROR: "Page load error"
|
||||
STR_EMPTY_FILE: "Empty file"
|
||||
STR_OUT_OF_BOUNDS: "Out of bounds"
|
||||
STR_LOADING: "Loading..."
|
||||
STR_LOAD_XTC_FAILED: "Failed to load XTC"
|
||||
STR_LOAD_TXT_FAILED: "Failed to load TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Failed to load EPUB"
|
||||
STR_SD_CARD_ERROR: "SD card error"
|
||||
STR_WIFI_NETWORKS: "WiFi Networks"
|
||||
STR_NO_NETWORKS: "No networks found"
|
||||
STR_NETWORKS_FOUND: "%zu networks found"
|
||||
STR_SCANNING: "Scanning..."
|
||||
STR_CONNECTING: "Connecting..."
|
||||
STR_CONNECTED: "Connected!"
|
||||
STR_CONNECTION_FAILED: "Connection Failed"
|
||||
STR_CONNECTION_TIMEOUT: "Connection timeout"
|
||||
STR_FORGET_NETWORK: "Forget Network?"
|
||||
STR_SAVE_PASSWORD: "Save password for next time?"
|
||||
STR_REMOVE_PASSWORD: "Remove saved password?"
|
||||
STR_PRESS_OK_SCAN: "Press OK to scan again"
|
||||
STR_PRESS_ANY_CONTINUE: "Press any button to continue"
|
||||
STR_SELECT_HINT: "LEFT/RIGHT: Select | OK: Confirm"
|
||||
STR_HOW_CONNECT: "How would you like to connect?"
|
||||
STR_JOIN_NETWORK: "Join a Network"
|
||||
STR_CREATE_HOTSPOT: "Create Hotspot"
|
||||
STR_JOIN_DESC: "Connect to an existing WiFi network"
|
||||
STR_HOTSPOT_DESC: "Create a WiFi network others can join"
|
||||
STR_STARTING_HOTSPOT: "Starting Hotspot..."
|
||||
STR_HOTSPOT_MODE: "Hotspot Mode"
|
||||
STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network"
|
||||
STR_OPEN_URL_HINT: "Open this URL in your browser"
|
||||
STR_OR_HTTP_PREFIX: "or http://"
|
||||
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||
STR_CONNECT_WIRELESS: "Connect as Wireless Device"
|
||||
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||
STR_MAC_ADDRESS: "MAC address:"
|
||||
STR_CHECKING_WIFI: "Checking WiFi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password"
|
||||
STR_ENTER_TEXT: "Enter Text"
|
||||
STR_TO_PREFIX: "to "
|
||||
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Connecting to "
|
||||
STR_CALIBRE_CONNECTED_TO: "Connected to "
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Waiting for commands..."
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Connection failed, retrying)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre disconnected"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Waiting for transfer..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "If transfer fails, enable\\n'Ignore free space' in Calibre's\\nSmartDevice plugin settings."
|
||||
STR_CALIBRE_RECEIVING: "Receiving: "
|
||||
STR_CALIBRE_RECEIVED: "Received: "
|
||||
STR_CALIBRE_WAITING_MORE: "Waiting for more..."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Failed to create file"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Password required"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer interrupted"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\""
|
||||
STR_CAT_DISPLAY: "Display"
|
||||
STR_CAT_READER: "Reader"
|
||||
STR_CAT_CONTROLS: "Controls"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
STR_SLEEP_SCREEN: "Sleep Screen"
|
||||
STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
|
||||
STR_STATUS_BAR: "Status Bar"
|
||||
STR_HIDE_BATTERY: "Hide Battery %"
|
||||
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
||||
STR_TEXT_AA: "Text Anti-Aliasing"
|
||||
STR_SHORT_PWR_BTN: "Short Power Button Click"
|
||||
STR_ORIENTATION: "Reading Orientation"
|
||||
STR_FRONT_BTN_LAYOUT: "Front Button Layout"
|
||||
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
||||
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
|
||||
STR_FONT_FAMILY: "Reader Font Family"
|
||||
STR_EXT_READER_FONT: "External Reader Font"
|
||||
STR_EXT_CHINESE_FONT: "Reader Font"
|
||||
STR_EXT_UI_FONT: "UI Font"
|
||||
STR_FONT_SIZE: "UI Font Size"
|
||||
STR_LINE_SPACING: "Reader Line Spacing"
|
||||
STR_ASCII_LETTER_SPACING: "ASCII Letter Spacing"
|
||||
STR_ASCII_DIGIT_SPACING: "ASCII Digit Spacing"
|
||||
STR_CJK_SPACING: "CJK Spacing"
|
||||
STR_COLOR_MODE: "Color Mode"
|
||||
STR_SCREEN_MARGIN: "Reader Screen Margin"
|
||||
STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
|
||||
STR_HYPHENATION: "Hyphenation"
|
||||
STR_TIME_TO_SLEEP: "Time to Sleep"
|
||||
STR_REFRESH_FREQ: "Refresh Frequency"
|
||||
STR_CALIBRE_SETTINGS: "Calibre Settings"
|
||||
STR_KOREADER_SYNC: "KOReader Sync"
|
||||
STR_CHECK_UPDATES: "Check for updates"
|
||||
STR_LANGUAGE: "Language"
|
||||
STR_SELECT_WALLPAPER: "Select Wallpaper"
|
||||
STR_CLEAR_READING_CACHE: "Clear Reading Cache"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Username"
|
||||
STR_PASSWORD: "Password"
|
||||
STR_SYNC_SERVER_URL: "Sync Server URL"
|
||||
STR_DOCUMENT_MATCHING: "Document Matching"
|
||||
STR_AUTHENTICATE: "Authenticate"
|
||||
STR_KOREADER_USERNAME: "KOReader Username"
|
||||
STR_KOREADER_PASSWORD: "KOReader Password"
|
||||
STR_FILENAME: "Filename"
|
||||
STR_BINARY: "Binary"
|
||||
STR_SET_CREDENTIALS_FIRST: "Set credentials first"
|
||||
STR_WIFI_CONN_FAILED: "WiFi connection failed"
|
||||
STR_AUTHENTICATING: "Authenticating..."
|
||||
STR_AUTH_SUCCESS: "Successfully authenticated!"
|
||||
STR_KOREADER_AUTH: "KOReader Auth"
|
||||
STR_SYNC_READY: "KOReader sync is ready to use"
|
||||
STR_AUTH_FAILED: "Authentication Failed"
|
||||
STR_DONE: "Done"
|
||||
STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data."
|
||||
STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed"
|
||||
STR_CLEAR_CACHE_WARNING_4: "when opened again."
|
||||
STR_CLEARING_CACHE: "Clearing cache..."
|
||||
STR_CACHE_CLEARED: "Cache Cleared"
|
||||
STR_ITEMS_REMOVED: "items removed"
|
||||
STR_FAILED_LOWER: "failed"
|
||||
STR_CLEAR_CACHE_FAILED: "Failed to clear cache"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Check serial output for details"
|
||||
STR_DARK: "Dark"
|
||||
STR_LIGHT: "Light"
|
||||
STR_CUSTOM: "Custom"
|
||||
STR_COVER: "Cover"
|
||||
STR_NONE_OPT: "None"
|
||||
STR_FIT: "Fit"
|
||||
STR_CROP: "Crop"
|
||||
STR_NO_PROGRESS: "No Progress"
|
||||
STR_FULL_OPT: "Full"
|
||||
STR_NEVER: "Never"
|
||||
STR_IN_READER: "In Reader"
|
||||
STR_ALWAYS: "Always"
|
||||
STR_IGNORE: "Ignore"
|
||||
STR_SLEEP: "Sleep"
|
||||
STR_PAGE_TURN: "Page Turn"
|
||||
STR_PORTRAIT: "Portrait"
|
||||
STR_LANDSCAPE_CW: "Landscape CW"
|
||||
STR_INVERTED: "Inverted"
|
||||
STR_LANDSCAPE_CCW: "Landscape CCW"
|
||||
STR_FRONT_LAYOUT_BCLR: "Bck, Cnfrm, Lft, Rght"
|
||||
STR_FRONT_LAYOUT_LRBC: "Lft, Rght, Bck, Cnfrm"
|
||||
STR_FRONT_LAYOUT_LBCR: "Lft, Bck, Cnfrm, Rght"
|
||||
STR_PREV_NEXT: "Prev/Next"
|
||||
STR_NEXT_PREV: "Next/Prev"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Small"
|
||||
STR_MEDIUM: "Medium"
|
||||
STR_LARGE: "Large"
|
||||
STR_X_LARGE: "X Large"
|
||||
STR_TIGHT: "Tight"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Wide"
|
||||
STR_JUSTIFY: "Justify"
|
||||
STR_ALIGN_LEFT: "Left"
|
||||
STR_CENTER: "Center"
|
||||
STR_ALIGN_RIGHT: "Right"
|
||||
STR_MIN_1: "1 min"
|
||||
STR_MIN_5: "5 min"
|
||||
STR_MIN_10: "10 min"
|
||||
STR_MIN_15: "15 min"
|
||||
STR_MIN_30: "30 min"
|
||||
STR_PAGES_1: "1 page"
|
||||
STR_PAGES_5: "5 pages"
|
||||
STR_PAGES_10: "10 pages"
|
||||
STR_PAGES_15: "15 pages"
|
||||
STR_PAGES_30: "30 pages"
|
||||
STR_UPDATE: "Update"
|
||||
STR_CHECKING_UPDATE: "Checking for update..."
|
||||
STR_NEW_UPDATE: "New update available!"
|
||||
STR_CURRENT_VERSION: "Current Version: "
|
||||
STR_NEW_VERSION: "New Version: "
|
||||
STR_UPDATING: "Updating..."
|
||||
STR_NO_UPDATE: "No update available"
|
||||
STR_UPDATE_FAILED: "Update failed"
|
||||
STR_UPDATE_COMPLETE: "Update complete"
|
||||
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
|
||||
STR_EXTERNAL_FONT: "External Font"
|
||||
STR_BUILTIN_DISABLED: "Built-in (Disabled)"
|
||||
STR_NO_ENTRIES: "No entries found"
|
||||
STR_DOWNLOADING: "Downloading..."
|
||||
STR_DOWNLOAD_FAILED: "Download failed"
|
||||
STR_ERROR_MSG: "Error:"
|
||||
STR_UNNAMED: "Unnamed"
|
||||
STR_NO_SERVER_URL: "No server URL configured"
|
||||
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||
STR_NETWORK_PREFIX: "Network: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP Address: "
|
||||
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Error: Network not found"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
|
||||
STR_SD_CARD: "SD card"
|
||||
STR_BACK: "« Back"
|
||||
STR_EXIT: "« Exit"
|
||||
STR_HOME: "« Home"
|
||||
STR_SAVE: "« Save"
|
||||
STR_SELECT: "Select"
|
||||
STR_TOGGLE: "Toggle"
|
||||
STR_CONFIRM: "Confirm"
|
||||
STR_CANCEL: "Cancel"
|
||||
STR_CONNECT: "Connect"
|
||||
STR_OPEN: "Open"
|
||||
STR_DOWNLOAD: "Download"
|
||||
STR_RETRY: "Retry"
|
||||
STR_YES: "Yes"
|
||||
STR_NO: "No"
|
||||
STR_STATE_ON: "ON"
|
||||
STR_STATE_OFF: "OFF"
|
||||
STR_SET: "Set"
|
||||
STR_NOT_SET: "Not Set"
|
||||
STR_DIR_LEFT: "Left"
|
||||
STR_DIR_RIGHT: "Right"
|
||||
STR_DIR_UP: "Up"
|
||||
STR_DIR_DOWN: "Down"
|
||||
STR_CAPS_ON: "CAPS"
|
||||
STR_CAPS_OFF: "caps"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[ON]"
|
||||
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
|
||||
STR_FILTER_CONTRAST: "Contrast"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Percentage"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Full w/ Book Bar"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Book Bar Only"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar"
|
||||
STR_UI_THEME: "UI Theme"
|
||||
STR_THEME_CLASSIC: "Classic"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
||||
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||
STR_OPDS_BROWSER: "OPDS Browser"
|
||||
STR_COVER_CUSTOM: "Cover + Custom"
|
||||
STR_RECENTS: "Recents"
|
||||
STR_MENU_RECENT_BOOKS: "Recent Books"
|
||||
STR_NO_RECENT_BOOKS: "No recent books"
|
||||
STR_CALIBRE_DESC: "Use Calibre wireless device transfers"
|
||||
STR_FORGET_AND_REMOVE: "Forget network and remove saved password?"
|
||||
STR_FORGET_BUTTON: "Forget network"
|
||||
STR_CALIBRE_STARTING: "Starting Calibre..."
|
||||
STR_CALIBRE_SETUP: "Setup"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Clear"
|
||||
STR_DEFAULT_VALUE: "Default"
|
||||
STR_REMAP_PROMPT: "Press a front button for each role"
|
||||
STR_UNASSIGNED: "Unassigned"
|
||||
STR_ALREADY_ASSIGNED: "Already assigned"
|
||||
STR_REMAP_RESET_HINT: "Side button Up: Reset to default layout"
|
||||
STR_REMAP_CANCEL_HINT: "Side button Down: Cancel remapping"
|
||||
STR_HW_BACK_LABEL: "Back (1st button)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
||||
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
||||
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
||||
STR_GO_TO_PERCENT: "Go to %"
|
||||
STR_GO_HOME_BUTTON: "Go Home"
|
||||
STR_SYNC_PROGRESS: "Sync Progress"
|
||||
STR_DELETE_CACHE: "Delete Book Cache"
|
||||
STR_CHAPTER_PREFIX: "Chapter: "
|
||||
STR_PAGES_SEPARATOR: " pages | "
|
||||
STR_BOOK_PREFIX: "Book: "
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "LOCK"
|
||||
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
|
||||
STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%"
|
||||
STR_SYNCING_TIME: "Syncing time..."
|
||||
STR_CALC_HASH: "Calculating document hash..."
|
||||
STR_HASH_FAILED: "Failed to calculate document hash"
|
||||
STR_FETCH_PROGRESS: "Fetching remote progress..."
|
||||
STR_UPLOAD_PROGRESS: "Uploading progress..."
|
||||
STR_NO_CREDENTIALS_MSG: "No credentials configured"
|
||||
STR_KOREADER_SETUP_HINT: "Set up KOReader account in Settings"
|
||||
STR_PROGRESS_FOUND: "Progress found!"
|
||||
STR_REMOTE_LABEL: "Remote:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% overall"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% overall"
|
||||
STR_DEVICE_FROM_FORMAT: " From: %s"
|
||||
STR_APPLY_REMOTE: "Apply remote progress"
|
||||
STR_UPLOAD_LOCAL: "Upload local progress"
|
||||
STR_NO_REMOTE_MSG: "No remote progress found"
|
||||
STR_UPLOAD_PROMPT: "Upload current position?"
|
||||
STR_UPLOAD_SUCCESS: "Progress uploaded!"
|
||||
STR_SYNC_FAILED_MSG: "Sync failed"
|
||||
STR_SECTION_PREFIX: "Section "
|
||||
STR_UPLOAD: "Upload"
|
||||
STR_BOOK_S_STYLE: "Book's Style"
|
||||
STR_EMBEDDED_STYLE: "Embedded Style"
|
||||
STR_OPDS_SERVER_URL: "OPDS Server URL"
|
||||
STR_LETTERBOX_FILL: "Letterbox Fill"
|
||||
STR_DITHERED: "Dithered"
|
||||
STR_SOLID: "Solid"
|
||||
STR_ADD_BOOKMARK: "Add Bookmark"
|
||||
STR_REMOVE_BOOKMARK: "Remove Bookmark"
|
||||
STR_LOOKUP_WORD: "Lookup Word"
|
||||
STR_LOOKUP_HISTORY: "Lookup Word History"
|
||||
STR_GO_TO_BOOKMARK: "Go to Bookmark"
|
||||
STR_CLOSE_BOOK: "Close Book"
|
||||
STR_DELETE_DICT_CACHE: "Delete Dictionary Cache"
|
||||
STR_DEFAULT_OPTION: "Default"
|
||||
STR_BOOKMARK_ADDED: "Bookmark added"
|
||||
STR_BOOKMARK_REMOVED: "Bookmark removed"
|
||||
STR_DICT_CACHE_DELETED: "Dictionary cache deleted"
|
||||
STR_NO_CACHE_TO_DELETE: "No cache to delete"
|
||||
STR_TABLE_OF_CONTENTS: "Table of Contents"
|
||||
317
lib/I18n/translations/french.yaml
Normal file
317
lib/I18n/translations/french.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Français"
|
||||
_language_code: "FRENCH"
|
||||
_order: "2"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "DÉMARRAGE EN COURS"
|
||||
STR_SLEEPING: "VEILLE"
|
||||
STR_ENTERING_SLEEP: "Mise en veille…"
|
||||
STR_BROWSE_FILES: "Fichiers"
|
||||
STR_FILE_TRANSFER: "Transfert"
|
||||
STR_SETTINGS_TITLE: "Réglages"
|
||||
STR_CALIBRE_LIBRARY: "Bibliothèque Calibre"
|
||||
STR_CONTINUE_READING: "Continuer la lecture"
|
||||
STR_NO_OPEN_BOOK: "Aucun livre ouvert"
|
||||
STR_START_READING: "Lisez votre premier livre ci-dessous"
|
||||
STR_BOOKS: "Livres"
|
||||
STR_NO_BOOKS_FOUND: "Dossier vide"
|
||||
STR_SELECT_CHAPTER: "Choix du chapitre"
|
||||
STR_NO_CHAPTERS: "Aucun chapitre"
|
||||
STR_END_OF_BOOK: "Fin du livre"
|
||||
STR_EMPTY_CHAPTER: "Chapitre vide"
|
||||
STR_INDEXING: "Indexation en cours…"
|
||||
STR_MEMORY_ERROR: "Erreur de mémoire"
|
||||
STR_PAGE_LOAD_ERROR: "Erreur de chargement"
|
||||
STR_EMPTY_FILE: "Fichier vide"
|
||||
STR_OUT_OF_BOUNDS: "Dépassement de mémoire"
|
||||
STR_LOADING: "Chargement…"
|
||||
STR_LOAD_XTC_FAILED: "Erreur de chargement du fichier XTC"
|
||||
STR_LOAD_TXT_FAILED: "Erreur de chargement du fichier TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Erreur de chargement du fichier EPUB"
|
||||
STR_SD_CARD_ERROR: "Carte mémoire absente"
|
||||
STR_WIFI_NETWORKS: "Réseaux WiFi"
|
||||
STR_NO_NETWORKS: "Aucun réseau"
|
||||
STR_NETWORKS_FOUND: "%zu réseaux"
|
||||
STR_SCANNING: "Recherche de réseaux en cours…"
|
||||
STR_CONNECTING: "Connexion en cours…"
|
||||
STR_CONNECTED: "Connecté !"
|
||||
STR_CONNECTION_FAILED: "Échec de la connexion"
|
||||
STR_CONNECTION_TIMEOUT: "Délai de connexion dépassé"
|
||||
STR_FORGET_NETWORK: "Oublier ce réseau ?"
|
||||
STR_SAVE_PASSWORD: "Enregistrer le mot de passe ?"
|
||||
STR_REMOVE_PASSWORD: "Supprimer le mot de passe enregistré ?"
|
||||
STR_PRESS_OK_SCAN: "Appuyez sur OK pour détecter à nouveau"
|
||||
STR_PRESS_ANY_CONTINUE: "Appuyez sur une touche pour continuer"
|
||||
STR_SELECT_HINT: "GAUCHE/DROITE: Sélectionner | OK: Valider"
|
||||
STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?"
|
||||
STR_JOIN_NETWORK: "Connexion à un réseau"
|
||||
STR_CREATE_HOTSPOT: "Créer un point d’accès"
|
||||
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
|
||||
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis d’autres appareils"
|
||||
STR_STARTING_HOTSPOT: "Création du point d’accès en cours…"
|
||||
STR_HOTSPOT_MODE: "Mode point d’accès"
|
||||
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi"
|
||||
STR_OPEN_URL_HINT: "Ouvrez cette URL dans votre navigateur"
|
||||
STR_OR_HTTP_PREFIX: "ou http://"
|
||||
STR_SCAN_QR_HINT: "ou scannez le QR code avec votre téléphone"
|
||||
STR_CALIBRE_WIRELESS: "Connexion à Calibre sans fil"
|
||||
STR_CALIBRE_WEB_URL: "URL Web Calibre"
|
||||
STR_CONNECT_WIRELESS: "Se connecter comme appareil sans fil"
|
||||
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
|
||||
STR_MAC_ADDRESS: "Adresse MAC :"
|
||||
STR_CHECKING_WIFI: "Vérification du réseau WiFi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Entrez le mot de passe WiFi"
|
||||
STR_ENTER_TEXT: "Entrez le texte"
|
||||
STR_TO_PREFIX: "à "
|
||||
STR_CALIBRE_DISCOVERING: "Recherche de Calibre en cours…"
|
||||
STR_CALIBRE_CONNECTING_TO: "Connexion à "
|
||||
STR_CALIBRE_CONNECTED_TO: "Connecté à "
|
||||
STR_CALIBRE_WAITING_COMMANDS: "En attente de commandes…"
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Échec de la connexion, nouvelle tentative)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre déconnecté"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "En attente de transfert…"
|
||||
STR_CALIBRE_TRANSFER_HINT: "Si le transfert échoue, activez\\n’Ignorer l’espace libre’ dans les\\nparamètres du plugin SmartDevice de Calibre."
|
||||
STR_CALIBRE_RECEIVING: "Réception : "
|
||||
STR_CALIBRE_RECEIVED: "Reçus : "
|
||||
STR_CALIBRE_WAITING_MORE: "En attente de données supplémentaires…"
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Échec de la création du fichier"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Mot de passe requis"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : ‘Envoyer vers l’appareil’"
|
||||
STR_CALIBRE_INSTRUCTION_4: "“Gardez cet écran ouvert pendant le transfert”"
|
||||
STR_CAT_DISPLAY: "Affichage"
|
||||
STR_CAT_READER: "Lecteur"
|
||||
STR_CAT_CONTROLS: "Commandes"
|
||||
STR_CAT_SYSTEM: "Système"
|
||||
STR_SLEEP_SCREEN: "Écran de veille"
|
||||
STR_SLEEP_COVER_MODE: "Mode d’image de l’écran de veille"
|
||||
STR_STATUS_BAR: "Barre d’état"
|
||||
STR_HIDE_BATTERY: "Masquer % batterie"
|
||||
STR_EXTRA_SPACING: "Espacement des paragraphes"
|
||||
STR_TEXT_AA: "Lissage du texte"
|
||||
STR_SHORT_PWR_BTN: "Appui court bout. alim."
|
||||
STR_ORIENTATION: "Orientation de lecture"
|
||||
STR_FRONT_BTN_LAYOUT: "Disposition des boutons avant"
|
||||
STR_SIDE_BTN_LAYOUT: "Disposition des boutons latéraux"
|
||||
STR_LONG_PRESS_SKIP: "Appui long pour saut de chapitre"
|
||||
STR_FONT_FAMILY: "Police de caractères du lecteur"
|
||||
STR_EXT_READER_FONT: "Police externe"
|
||||
STR_EXT_CHINESE_FONT: "Police du lecteur"
|
||||
STR_EXT_UI_FONT: "Police de l’interface"
|
||||
STR_FONT_SIZE: "Taille du texte de l’interface"
|
||||
STR_LINE_SPACING: "Espacement des lignes"
|
||||
STR_ASCII_LETTER_SPACING: "Espacement des lettres ASCII"
|
||||
STR_ASCII_DIGIT_SPACING: "Espacement des chiffres ASCII"
|
||||
STR_CJK_SPACING: "Espacement CJK"
|
||||
STR_COLOR_MODE: "Mode couleur"
|
||||
STR_SCREEN_MARGIN: "Marges du lecteur"
|
||||
STR_PARA_ALIGNMENT: "Alignement des paragraphes"
|
||||
STR_HYPHENATION: "Césure"
|
||||
STR_TIME_TO_SLEEP: "Mise en veille automatique"
|
||||
STR_REFRESH_FREQ: "Fréquence de rafraîchissement"
|
||||
STR_CALIBRE_SETTINGS: "Réglages Calibre"
|
||||
STR_KOREADER_SYNC: "Synchronisation KOReader"
|
||||
STR_CHECK_UPDATES: "Mise à jour"
|
||||
STR_LANGUAGE: "Langue"
|
||||
STR_SELECT_WALLPAPER: "Fond d’écran"
|
||||
STR_CLEAR_READING_CACHE: "Vider le cache de lecture"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Nom d’utilisateur"
|
||||
STR_PASSWORD: "Mot de passe"
|
||||
STR_SYNC_SERVER_URL: "URL du serveur"
|
||||
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||
STR_AUTHENTICATE: "Se connecter"
|
||||
STR_KOREADER_USERNAME: "Nom d’utilisateur"
|
||||
STR_KOREADER_PASSWORD: "Mot de passe"
|
||||
STR_FILENAME: "Nom de fichier"
|
||||
STR_BINARY: "Binaire"
|
||||
STR_SET_CREDENTIALS_FIRST: "Identifiants manquants"
|
||||
STR_WIFI_CONN_FAILED: "Échec de connexion WiFi"
|
||||
STR_AUTHENTICATING: "Connexion en cours…"
|
||||
STR_AUTH_SUCCESS: "Connexion réussie !"
|
||||
STR_KOREADER_AUTH: "Auth KOReader"
|
||||
STR_SYNC_READY: "Synchronisation KOReader prête"
|
||||
STR_AUTH_FAILED: "Échec de la connexion"
|
||||
STR_DONE: "OK"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Le cache de votre bibliothèque sera entièrement vidé"
|
||||
STR_CLEAR_CACHE_WARNING_2: "Votre progression de lecture sera perdue !"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Les livres devront être réindexés"
|
||||
STR_CLEAR_CACHE_WARNING_4: "à leur prochaine ouverture."
|
||||
STR_CLEARING_CACHE: "Suppression du cache…"
|
||||
STR_CACHE_CLEARED: "Cache supprimé"
|
||||
STR_ITEMS_REMOVED: "éléments supprimés"
|
||||
STR_FAILED_LOWER: "ont échoué"
|
||||
STR_CLEAR_CACHE_FAILED: "Échec de la suppression du cache"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Vérifiez la console série pour plus de détails"
|
||||
STR_DARK: "Sombre"
|
||||
STR_LIGHT: "Clair"
|
||||
STR_CUSTOM: "Custom"
|
||||
STR_COVER: "Couverture"
|
||||
STR_NONE_OPT: "Aucun"
|
||||
STR_FIT: "Ajusté"
|
||||
STR_CROP: "Rogné"
|
||||
STR_NO_PROGRESS: "Sans progression"
|
||||
STR_FULL_OPT: "Complète"
|
||||
STR_NEVER: "Jamais"
|
||||
STR_IN_READER: "Dans le lecteur"
|
||||
STR_ALWAYS: "Toujours"
|
||||
STR_IGNORE: "Ignorer"
|
||||
STR_SLEEP: "Mise en veille"
|
||||
STR_PAGE_TURN: "Page suivante"
|
||||
STR_PORTRAIT: "Portrait"
|
||||
STR_LANDSCAPE_CW: "Paysage"
|
||||
STR_INVERTED: "Inversé"
|
||||
STR_LANDSCAPE_CCW: "Paysage inversé"
|
||||
STR_FRONT_LAYOUT_BCLR: "Ret, OK, Gauche, Droite"
|
||||
STR_FRONT_LAYOUT_LRBC: "Gauche, Droite, Ret, OK"
|
||||
STR_FRONT_LAYOUT_LBCR: "Gauche, Ret, OK, Droite"
|
||||
STR_PREV_NEXT: "Prec/Suiv"
|
||||
STR_NEXT_PREV: "Suiv/Prec"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Petite"
|
||||
STR_MEDIUM: "Moyenne"
|
||||
STR_LARGE: "Grande"
|
||||
STR_X_LARGE: "T Grande"
|
||||
STR_TIGHT: "Serré"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Large"
|
||||
STR_JUSTIFY: "Justifier"
|
||||
STR_ALIGN_LEFT: "Gauche"
|
||||
STR_CENTER: "Centre"
|
||||
STR_ALIGN_RIGHT: "Droite"
|
||||
STR_MIN_1: "1 min"
|
||||
STR_MIN_5: "5 min"
|
||||
STR_MIN_10: "10 min"
|
||||
STR_MIN_15: "15 min"
|
||||
STR_MIN_30: "30 min"
|
||||
STR_PAGES_1: "1 page"
|
||||
STR_PAGES_5: "5 pages"
|
||||
STR_PAGES_10: "10 pages"
|
||||
STR_PAGES_15: "15 pages"
|
||||
STR_PAGES_30: "30 pages"
|
||||
STR_UPDATE: "Mise à jour"
|
||||
STR_CHECKING_UPDATE: "Recherche de mises à jour en cours…"
|
||||
STR_NEW_UPDATE: "Nouvelle mise à jour disponible !"
|
||||
STR_CURRENT_VERSION: "Version actuelle :"
|
||||
STR_NEW_VERSION: "Nouvelle version : "
|
||||
STR_UPDATING: "Mise à jour en cours…"
|
||||
STR_NO_UPDATE: "Aucune mise à jour disponible"
|
||||
STR_UPDATE_FAILED: "Échec de la mise à jour"
|
||||
STR_UPDATE_COMPLETE: "Mise à jour effectuée"
|
||||
STR_POWER_ON_HINT: "Maintenir le bouton d’alimentation pour redémarrer"
|
||||
STR_EXTERNAL_FONT: "Police externe"
|
||||
STR_BUILTIN_DISABLED: "Intégrée (désactivée)"
|
||||
STR_NO_ENTRIES: "Aucune entrée trouvée"
|
||||
STR_DOWNLOADING: "Téléchargement en cours…"
|
||||
STR_DOWNLOAD_FAILED: "Échec du téléchargement"
|
||||
STR_ERROR_MSG: "Erreur : "
|
||||
STR_UNNAMED: "Sans titre"
|
||||
STR_NO_SERVER_URL: "Aucune URL serveur configurée"
|
||||
STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux"
|
||||
STR_PARSE_FEED_FAILED: "Échec de l’analyse du flux"
|
||||
STR_NETWORK_PREFIX: "Réseau : "
|
||||
STR_IP_ADDRESS_PREFIX: "Adresse IP : "
|
||||
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
|
||||
STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Erreur : Délai de connexion dépassé"
|
||||
STR_SD_CARD: "Carte SD"
|
||||
STR_BACK: "« Retour"
|
||||
STR_EXIT: "« Sortie"
|
||||
STR_HOME: "« Accueil"
|
||||
STR_SAVE: "« Sauver"
|
||||
STR_SELECT: "OK"
|
||||
STR_TOGGLE: "Modifier"
|
||||
STR_CONFIRM: "Confirmer"
|
||||
STR_CANCEL: "Annuler"
|
||||
STR_CONNECT: "OK"
|
||||
STR_OPEN: "Ouvrir"
|
||||
STR_DOWNLOAD: "Télécharger"
|
||||
STR_RETRY: "Réessayer"
|
||||
STR_YES: "Oui"
|
||||
STR_NO: "Non"
|
||||
STR_STATE_ON: "ON"
|
||||
STR_STATE_OFF: "OFF"
|
||||
STR_SET: "Défini"
|
||||
STR_NOT_SET: "Non défini"
|
||||
STR_DIR_LEFT: "Gauche"
|
||||
STR_DIR_RIGHT: "Droite"
|
||||
STR_DIR_UP: "Haut"
|
||||
STR_DIR_DOWN: "Bas"
|
||||
STR_CAPS_ON: "MAJ"
|
||||
STR_CAPS_OFF: "maj"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[ON]"
|
||||
STR_SLEEP_COVER_FILTER: "Filtre affichage veille"
|
||||
STR_FILTER_CONTRAST: "Contraste"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Complète + %"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Complète + barre livre"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Barre livre"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre"
|
||||
STR_UI_THEME: "Thème de l’interface"
|
||||
STR_THEME_CLASSIC: "Classique"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil"
|
||||
STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant"
|
||||
STR_OPDS_BROWSER: "Navigateur OPDS"
|
||||
STR_COVER_CUSTOM: "Couverture + Custom"
|
||||
STR_RECENTS: "Récents"
|
||||
STR_MENU_RECENT_BOOKS: "Livres récents"
|
||||
STR_NO_RECENT_BOOKS: "Aucun livre récent"
|
||||
STR_CALIBRE_DESC: "Utiliser les transferts sans fil Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Oublier le réseau et supprimer le mot de passe enregistré ?"
|
||||
STR_FORGET_BUTTON: "Oublier le réseau"
|
||||
STR_CALIBRE_STARTING: "Démarrage de Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configuration"
|
||||
STR_CALIBRE_STATUS: "Statut"
|
||||
STR_CLEAR_BUTTON: "Effacer"
|
||||
STR_DEFAULT_VALUE: "Défaut"
|
||||
STR_REMAP_PROMPT: "Appuyez sur un bouton avant pour chaque rôle"
|
||||
STR_UNASSIGNED: "Non assigné"
|
||||
STR_ALREADY_ASSIGNED: "Déjà assigné"
|
||||
STR_REMAP_RESET_HINT: "Bouton latéral haut : Réinitialiser"
|
||||
STR_REMAP_CANCEL_HINT: "Bouton latéral bas : Annuler le réglage"
|
||||
STR_HW_BACK_LABEL: "Retour (1er bouton)"
|
||||
STR_HW_CONFIRM_LABEL: "OK (2ème bouton)"
|
||||
STR_HW_LEFT_LABEL: "Gauche (3ème bouton)"
|
||||
STR_HW_RIGHT_LABEL: "Droite (4ème bouton)"
|
||||
STR_GO_TO_PERCENT: "Aller à %"
|
||||
STR_GO_HOME_BUTTON: "Aller à l’accueil"
|
||||
STR_SYNC_PROGRESS: "Synchroniser la progression"
|
||||
STR_DELETE_CACHE: "Supprimer le cache du livre"
|
||||
STR_CHAPTER_PREFIX: "Chapitre : "
|
||||
STR_PAGES_SEPARATOR: " pages | "
|
||||
STR_BOOK_PREFIX: "Livre : "
|
||||
STR_KBD_SHIFT: "maj"
|
||||
STR_KBD_SHIFT_CAPS: "MAJ"
|
||||
STR_KBD_LOCK: "VERR MAJ"
|
||||
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
||||
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
|
||||
STR_SYNCING_TIME: "Synchronisation de l’heure…"
|
||||
STR_CALC_HASH: "Calcul du hash du document…"
|
||||
STR_HASH_FAILED: "Échec du calcul du hash du document"
|
||||
STR_FETCH_PROGRESS: "Téléchargement de la progression…"
|
||||
STR_UPLOAD_PROGRESS: "Envoi de la progression…"
|
||||
STR_NO_CREDENTIALS_MSG: "Aucun identifiant configuré"
|
||||
STR_KOREADER_SETUP_HINT: "Configurez le compte KOReader dans les réglages"
|
||||
STR_PROGRESS_FOUND: "Progression trouvée !"
|
||||
STR_REMOTE_LABEL: "En ligne :"
|
||||
STR_LOCAL_LABEL: "Locale :"
|
||||
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% au total"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% au total"
|
||||
STR_DEVICE_FROM_FORMAT: " De : %s"
|
||||
STR_APPLY_REMOTE: "Appliquer la progression en ligne"
|
||||
STR_UPLOAD_LOCAL: "Envoyer la progression locale"
|
||||
STR_NO_REMOTE_MSG: "Aucune progression en ligne trouvée"
|
||||
STR_UPLOAD_PROMPT: "Envoyer la position actuelle ?"
|
||||
STR_UPLOAD_SUCCESS: "Progression envoyée !"
|
||||
STR_SYNC_FAILED_MSG: "Échec de la synchronisation"
|
||||
STR_SECTION_PREFIX: "Section "
|
||||
STR_UPLOAD: "Envoi"
|
||||
STR_BOOK_S_STYLE: "Style du livre"
|
||||
STR_EMBEDDED_STYLE: "Style intégré"
|
||||
STR_OPDS_SERVER_URL: "URL du serveur OPDS"
|
||||
317
lib/I18n/translations/german.yaml
Normal file
317
lib/I18n/translations/german.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Deutsch"
|
||||
_language_code: "GERMAN"
|
||||
_order: "3"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "STARTEN"
|
||||
STR_SLEEPING: "STANDBY"
|
||||
STR_ENTERING_SLEEP: "Standby..."
|
||||
STR_BROWSE_FILES: "Durchsuchen"
|
||||
STR_FILE_TRANSFER: "Datentransfer"
|
||||
STR_SETTINGS_TITLE: "Einstellungen"
|
||||
STR_CALIBRE_LIBRARY: "Calibre-Bibliothek"
|
||||
STR_CONTINUE_READING: "Weiterlesen"
|
||||
STR_NO_OPEN_BOOK: "Aktuell kein Buch"
|
||||
STR_START_READING: "Lesen beginnen"
|
||||
STR_BOOKS: "Bücher"
|
||||
STR_NO_BOOKS_FOUND: "Keine Bücher"
|
||||
STR_SELECT_CHAPTER: "Kapitel auswählen"
|
||||
STR_NO_CHAPTERS: "Keine Kapitel"
|
||||
STR_END_OF_BOOK: "Buchende"
|
||||
STR_EMPTY_CHAPTER: "Kapitelende"
|
||||
STR_INDEXING: "Indexieren…"
|
||||
STR_MEMORY_ERROR: "Speicherfehler"
|
||||
STR_PAGE_LOAD_ERROR: "Seitenladefehler"
|
||||
STR_EMPTY_FILE: "Leere Datei"
|
||||
STR_OUT_OF_BOUNDS: "Zu groß"
|
||||
STR_LOADING: "Laden…"
|
||||
STR_LOAD_XTC_FAILED: "Ladefehler bei XTC"
|
||||
STR_LOAD_TXT_FAILED: "Ladefehler bei TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Ladefehler bei EPUB"
|
||||
STR_SD_CARD_ERROR: "SD-Karten-Fehler"
|
||||
STR_WIFI_NETWORKS: "WLAN-Netzwerke"
|
||||
STR_NO_NETWORKS: "Kein WLAN gefunden"
|
||||
STR_NETWORKS_FOUND: "%zu WLAN-Netzwerke gefunden"
|
||||
STR_SCANNING: "Suchen..."
|
||||
STR_CONNECTING: "Verbinden..."
|
||||
STR_CONNECTED: "Verbunden!"
|
||||
STR_CONNECTION_FAILED: "Verbindungsfehler"
|
||||
STR_CONNECTION_TIMEOUT: "Verbindungs-Timeout"
|
||||
STR_FORGET_NETWORK: "WLAN vergessen?"
|
||||
STR_SAVE_PASSWORD: "Passwort speichern?"
|
||||
STR_REMOVE_PASSWORD: "Passwort entfernen?"
|
||||
STR_PRESS_OK_SCAN: "OK für neue Suche"
|
||||
STR_PRESS_ANY_CONTINUE: "Beliebige Taste drücken"
|
||||
STR_SELECT_HINT: "links/rechts: Auswahl | OK: Best"
|
||||
STR_HOW_CONNECT: "Wie möchtest du dich verbinden?"
|
||||
STR_JOIN_NETWORK: "Netzwerk beitreten"
|
||||
STR_CREATE_HOTSPOT: "Hotspot erstellen"
|
||||
STR_JOIN_DESC: "Mit einem bestehenden WLAN verbinden"
|
||||
STR_HOTSPOT_DESC: "WLAN für andere erstellen"
|
||||
STR_STARTING_HOTSPOT: "Hotspot starten…"
|
||||
STR_HOTSPOT_MODE: "Hotspot-Modus"
|
||||
STR_CONNECT_WIFI_HINT: "Gerät mit diesem WLAN verbinden"
|
||||
STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
|
||||
STR_OR_HTTP_PREFIX: "oder http://"
|
||||
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre-Web-URL"
|
||||
STR_CONNECT_WIRELESS: "Als Drahtlos-Gerät hinzufügen"
|
||||
STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
|
||||
STR_MAC_ADDRESS: "MAC-Adresse:"
|
||||
STR_CHECKING_WIFI: "WLAN prüfen…"
|
||||
STR_ENTER_WIFI_PASSWORD: "WLAN-Passwort eingeben"
|
||||
STR_ENTER_TEXT: "Text eingeben"
|
||||
STR_TO_PREFIX: "bis"
|
||||
STR_CALIBRE_DISCOVERING: "Calibre finden..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Verbinden mit"
|
||||
STR_CALIBRE_CONNECTED_TO: "Verbunden mit"
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Auf Befehle warten…"
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Keine Verbindung, wiederholen)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre getrennt"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Auf Übertragung warten..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "Bei Übertragungsfehler \\n'Freien Speicher ign.' in den\\nCalibre-Einstellungen einschalten."
|
||||
STR_CALIBRE_RECEIVING: "Empfange:"
|
||||
STR_CALIBRE_RECEIVED: "Empfangen:"
|
||||
STR_CALIBRE_WAITING_MORE: "Auf mehr warten…"
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Speicherfehler"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Passwort nötig"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Übertragung unterbrochen"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader-Plugin installieren"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Mit selbem WLAN verbinden"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"An Gerät senden\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "Bildschirm beim Senden offenlassen"
|
||||
STR_CAT_DISPLAY: "Anzeige"
|
||||
STR_CAT_READER: "Lesen"
|
||||
STR_CAT_CONTROLS: "Bedienung"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
STR_SLEEP_SCREEN: "Standby-Bild"
|
||||
STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
|
||||
STR_STATUS_BAR: "Statusleiste"
|
||||
STR_HIDE_BATTERY: "Batterie % ausblenden"
|
||||
STR_EXTRA_SPACING: "Absatzabstand"
|
||||
STR_TEXT_AA: "Schriftglättung"
|
||||
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
|
||||
STR_ORIENTATION: "Leseausrichtung"
|
||||
STR_FRONT_BTN_LAYOUT: "Vorderes Tastenlayout"
|
||||
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
||||
STR_LONG_PRESS_SKIP: "Langes Drücken springt Kap."
|
||||
STR_FONT_FAMILY: "Lese-Schriftfamilie"
|
||||
STR_EXT_READER_FONT: "Externe Schriftart"
|
||||
STR_EXT_CHINESE_FONT: "Lese-Schriftart"
|
||||
STR_EXT_UI_FONT: "Menü-Schriftart"
|
||||
STR_FONT_SIZE: "Schriftgröße"
|
||||
STR_LINE_SPACING: "Lese-Zeilenabstand"
|
||||
STR_ASCII_LETTER_SPACING: "ASCII-Zeichenabstand"
|
||||
STR_ASCII_DIGIT_SPACING: "ASCII-Ziffernabstand"
|
||||
STR_CJK_SPACING: "CJK-Zeichenabstand"
|
||||
STR_COLOR_MODE: "Farbmodus"
|
||||
STR_SCREEN_MARGIN: "Lese-Seitenränder"
|
||||
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
|
||||
STR_HYPHENATION: "Silbentrennung"
|
||||
STR_TIME_TO_SLEEP: "Standby nach"
|
||||
STR_REFRESH_FREQ: "Anti-Ghosting nach"
|
||||
STR_CALIBRE_SETTINGS: "Calibre-Einstellungen"
|
||||
STR_KOREADER_SYNC: "KOReader-Synchr."
|
||||
STR_CHECK_UPDATES: "Nach Updates suchen"
|
||||
STR_LANGUAGE: "Sprache"
|
||||
STR_SELECT_WALLPAPER: "Bildauswahl Standby"
|
||||
STR_CLEAR_READING_CACHE: "Lese-Cache leeren"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Benutzername"
|
||||
STR_PASSWORD: "Passwort nötig"
|
||||
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
||||
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
||||
STR_AUTHENTICATE: "Authentifizieren"
|
||||
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
||||
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
||||
STR_FILENAME: "Dateiname"
|
||||
STR_BINARY: "Binärdatei"
|
||||
STR_SET_CREDENTIALS_FIRST: "Zuerst anmelden"
|
||||
STR_WIFI_CONN_FAILED: "WLAN-Verbindung fehlgeschlagen"
|
||||
STR_AUTHENTICATING: "Authentifizieren…"
|
||||
STR_AUTH_SUCCESS: "Erfolgreich authentifiziert!"
|
||||
STR_KOREADER_AUTH: "KOReader-Auth"
|
||||
STR_SYNC_READY: "KOReader-Synchronisierung bereit"
|
||||
STR_AUTH_FAILED: "Authentifizierung fehlg."
|
||||
STR_DONE: "Erledigt"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Alle Buch-Caches werden geleert."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Lesefortschritt wird gelöscht!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Bücher müssen beim Öffnen"
|
||||
STR_CLEAR_CACHE_WARNING_4: "neu eingelesen werden."
|
||||
STR_CLEARING_CACHE: "Cache leeren…"
|
||||
STR_CACHE_CLEARED: "Cache geleert"
|
||||
STR_ITEMS_REMOVED: "Einträge entfernt"
|
||||
STR_FAILED_LOWER: "fehlgeschlagen"
|
||||
STR_CLEAR_CACHE_FAILED: "Fehler beim Cache-Leeren"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Serielle Ausgabe prüfen"
|
||||
STR_DARK: "Dunkel"
|
||||
STR_LIGHT: "Hell"
|
||||
STR_CUSTOM: "Eigenes"
|
||||
STR_COVER: "Umschlag"
|
||||
STR_NONE_OPT: "Leer"
|
||||
STR_FIT: "Anpassen"
|
||||
STR_CROP: "Zuschnitt"
|
||||
STR_NO_PROGRESS: "Ohne Fortschr."
|
||||
STR_FULL_OPT: "Vollst."
|
||||
STR_NEVER: "Nie"
|
||||
STR_IN_READER: "Beim Lesen"
|
||||
STR_ALWAYS: "Immer"
|
||||
STR_IGNORE: "Ignorieren"
|
||||
STR_SLEEP: "Standby"
|
||||
STR_PAGE_TURN: "Umblättern"
|
||||
STR_PORTRAIT: "Hochformat"
|
||||
STR_LANDSCAPE_CW: "Querformat rechts"
|
||||
STR_INVERTED: "Invertiert"
|
||||
STR_LANDSCAPE_CCW: "Querformat links"
|
||||
STR_FRONT_LAYOUT_BCLR: "Zurück, Bst, L, R"
|
||||
STR_FRONT_LAYOUT_LRBC: "L, R, Zurück, Bst"
|
||||
STR_FRONT_LAYOUT_LBCR: "L, Zurück, Bst, R"
|
||||
STR_PREV_NEXT: "Zurück/Weiter"
|
||||
STR_NEXT_PREV: "Weiter/Zuürck"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Klein"
|
||||
STR_MEDIUM: "Mittel"
|
||||
STR_LARGE: "Groß"
|
||||
STR_X_LARGE: "Extragroß"
|
||||
STR_TIGHT: "Eng"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Breit"
|
||||
STR_JUSTIFY: "Blocksatz"
|
||||
STR_ALIGN_LEFT: "Links"
|
||||
STR_CENTER: "Zentriert"
|
||||
STR_ALIGN_RIGHT: "Rechts"
|
||||
STR_MIN_1: "1 Min"
|
||||
STR_MIN_5: "5 Min"
|
||||
STR_MIN_10: "10 Min"
|
||||
STR_MIN_15: "15 Min"
|
||||
STR_MIN_30: "30 Min"
|
||||
STR_PAGES_1: "1 Seite"
|
||||
STR_PAGES_5: "5 Seiten"
|
||||
STR_PAGES_10: "10 Seiten"
|
||||
STR_PAGES_15: "15 Seiten"
|
||||
STR_PAGES_30: "30 Seiten"
|
||||
STR_UPDATE: "Update"
|
||||
STR_CHECKING_UPDATE: "Update suchen…"
|
||||
STR_NEW_UPDATE: "Neues Update verfügbar!"
|
||||
STR_CURRENT_VERSION: "Aktuelle Version:"
|
||||
STR_NEW_VERSION: "Neue Version:"
|
||||
STR_UPDATING: "Aktualisiere…"
|
||||
STR_NO_UPDATE: "Kein Update verfügbar"
|
||||
STR_UPDATE_FAILED: "Updatefehler"
|
||||
STR_UPDATE_COMPLETE: "Update fertig"
|
||||
STR_POWER_ON_HINT: "An-Knopf lang drücken, um neuzustarten"
|
||||
STR_EXTERNAL_FONT: "Externe Schrift"
|
||||
STR_BUILTIN_DISABLED: "Vorinstalliert (aus)"
|
||||
STR_NO_ENTRIES: "Keine Einträge"
|
||||
STR_DOWNLOADING: "Herunterladen…"
|
||||
STR_DOWNLOAD_FAILED: "Ladefehler"
|
||||
STR_ERROR_MSG: "Fehler:"
|
||||
STR_UNNAMED: "Unbenannt"
|
||||
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
|
||||
STR_FETCH_FEED_FAILED: "Feedfehler"
|
||||
STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
|
||||
STR_NETWORK_PREFIX: "Netzwerk:"
|
||||
STR_IP_ADDRESS_PREFIX: "IP-Adresse:"
|
||||
STR_SCAN_QR_WIFI_HINT: "oder QR-Code mit dem Handy scannen für WLAN."
|
||||
STR_ERROR_GENERAL_FAILURE: "Fehler: Allgemeiner Fehler"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Fehler: Kein Netzwerk"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Fehler: Zeitüberschreitung"
|
||||
STR_SD_CARD: "SD-Karte"
|
||||
STR_BACK: "« Zurück"
|
||||
STR_EXIT: "« Verlassen"
|
||||
STR_HOME: "« Start"
|
||||
STR_SAVE: "« Speichern"
|
||||
STR_SELECT: "Auswahl"
|
||||
STR_TOGGLE: "Ändern"
|
||||
STR_CONFIRM: "Bestätigen"
|
||||
STR_CANCEL: "Abbrechen"
|
||||
STR_CONNECT: "Verbinden"
|
||||
STR_OPEN: "Öffnen"
|
||||
STR_DOWNLOAD: "Herunterladen"
|
||||
STR_RETRY: "Wiederh."
|
||||
STR_YES: "Ja"
|
||||
STR_NO: "Nein"
|
||||
STR_STATE_ON: "An"
|
||||
STR_STATE_OFF: "Aus"
|
||||
STR_SET: "Gesetzt"
|
||||
STR_NOT_SET: "Leer"
|
||||
STR_DIR_LEFT: "Links"
|
||||
STR_DIR_RIGHT: "Rechts"
|
||||
STR_DIR_UP: "Hoch"
|
||||
STR_DIR_DOWN: "Runter"
|
||||
STR_CAPS_ON: "UMSCH"
|
||||
STR_CAPS_OFF: "umsch"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[AN]"
|
||||
STR_SLEEP_COVER_FILTER: "Standby-Coverfilter"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Komplett + Prozent"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Komplett + Buch"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Nur Buch"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel"
|
||||
STR_UI_THEME: "System-Design"
|
||||
STR_THEME_CLASSIC: "Klassisch"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
|
||||
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
|
||||
STR_OPDS_BROWSER: "OPDS-Browser"
|
||||
STR_COVER_CUSTOM: "Umschlag + Eigenes"
|
||||
STR_RECENTS: "Zuletzt"
|
||||
STR_MENU_RECENT_BOOKS: "Zuletzt gelesen"
|
||||
STR_NO_RECENT_BOOKS: "Keine Bücher"
|
||||
STR_CALIBRE_DESC: "Calibre-Übertragung (WLAN)"
|
||||
STR_FORGET_AND_REMOVE: "WLAN entfernen & Passwort löschen?"
|
||||
STR_FORGET_BUTTON: "WLAN entfernen"
|
||||
STR_CALIBRE_STARTING: "Calibre starten…"
|
||||
STR_CALIBRE_SETUP: "Installation"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Leeren"
|
||||
STR_DEFAULT_VALUE: "Standard"
|
||||
STR_REMAP_PROMPT: "Entsprechende Vordertaste drücken"
|
||||
STR_UNASSIGNED: "Leer"
|
||||
STR_ALREADY_ASSIGNED: "Bereits zugeordnet"
|
||||
STR_REMAP_RESET_HINT: "Seitentaste hoch: Standard"
|
||||
STR_REMAP_CANCEL_HINT: "Seitentaste runter: Abbrechen"
|
||||
STR_HW_BACK_LABEL: "Zurück (1. Taste)"
|
||||
STR_HW_CONFIRM_LABEL: "Bestätigen (2. Taste)"
|
||||
STR_HW_LEFT_LABEL: "Links (3. Taste)"
|
||||
STR_HW_RIGHT_LABEL: "Rechts (4. Taste)"
|
||||
STR_GO_TO_PERCENT: "Gehe zu %"
|
||||
STR_GO_HOME_BUTTON: "Zum Anfang"
|
||||
STR_SYNC_PROGRESS: "Fortschritt synchronisieren"
|
||||
STR_DELETE_CACHE: "Buch-Cache leeren"
|
||||
STR_CHAPTER_PREFIX: "Kapitel:"
|
||||
STR_PAGES_SEPARATOR: " Seiten | "
|
||||
STR_BOOK_PREFIX: "Buch: "
|
||||
STR_KBD_SHIFT: "umsch"
|
||||
STR_KBD_SHIFT_CAPS: "UMSCH"
|
||||
STR_KBD_LOCK: "FESTST"
|
||||
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
|
||||
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
|
||||
STR_SYNCING_TIME: "Zeit synchonisieren…"
|
||||
STR_CALC_HASH: "Dokument-Hash berechnen…"
|
||||
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
|
||||
STR_FETCH_PROGRESS: "Externen Fortschritt abrufen..."
|
||||
STR_UPLOAD_PROGRESS: "Fortschritt hochladen…"
|
||||
STR_NO_CREDENTIALS_MSG: "Zugangsdaten fehlen"
|
||||
STR_KOREADER_SETUP_HINT: "KOReader-Konto unter Einst. anlegen"
|
||||
STR_PROGRESS_FOUND: "Gefunden!"
|
||||
STR_REMOTE_LABEL: "Extern:"
|
||||
STR_LOCAL_LABEL: "Lokal:"
|
||||
STR_PAGE_OVERALL_FORMAT: " Seite %d, %.2f%% insgesamt"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: " Seite %d/%d, %.2f%% insgesamt"
|
||||
STR_DEVICE_FROM_FORMAT: " Von: %s"
|
||||
STR_APPLY_REMOTE: "Ext. Fortschritt übern."
|
||||
STR_UPLOAD_LOCAL: "Lokalen Fortschritt hochl."
|
||||
STR_NO_REMOTE_MSG: "Kein externer Fortschritt"
|
||||
STR_UPLOAD_PROMPT: "Aktuelle Position hochladen?"
|
||||
STR_UPLOAD_SUCCESS: "Hochgeladen!"
|
||||
STR_SYNC_FAILED_MSG: "Fehlgeschlagen"
|
||||
STR_SECTION_PREFIX: "Abschnitt"
|
||||
STR_UPLOAD: "Hochladen"
|
||||
STR_BOOK_S_STYLE: "Buch-Stil"
|
||||
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
|
||||
STR_OPDS_SERVER_URL: "OPDS-Server-URL"
|
||||
317
lib/I18n/translations/portuguese.yaml
Normal file
317
lib/I18n/translations/portuguese.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Português (Brasil)"
|
||||
_language_code: "PORTUGUESE"
|
||||
_order: "5"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "INICIANDO"
|
||||
STR_SLEEPING: "EM REPOUSO"
|
||||
STR_ENTERING_SLEEP: "Entrando em repouso..."
|
||||
STR_BROWSE_FILES: "Arquivos"
|
||||
STR_FILE_TRANSFER: "Transferência"
|
||||
STR_SETTINGS_TITLE: "Configurações"
|
||||
STR_CALIBRE_LIBRARY: "Biblioteca do Calibre"
|
||||
STR_CONTINUE_READING: "Continuar lendo"
|
||||
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
|
||||
STR_START_READING: "Comece a ler abaixo"
|
||||
STR_BOOKS: "Livros"
|
||||
STR_NO_BOOKS_FOUND: "Nenhum livro encontrado"
|
||||
STR_SELECT_CHAPTER: "Escolher capítulo"
|
||||
STR_NO_CHAPTERS: "Sem capítulos"
|
||||
STR_END_OF_BOOK: "Fim do livro"
|
||||
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||
STR_INDEXING: "Indexando..."
|
||||
STR_MEMORY_ERROR: "Erro de memória"
|
||||
STR_PAGE_LOAD_ERROR: "Erro página"
|
||||
STR_EMPTY_FILE: "Arquivo vazio"
|
||||
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||
STR_LOADING: "Carregando..."
|
||||
STR_LOAD_XTC_FAILED: "Falha ao carregar XTC"
|
||||
STR_LOAD_TXT_FAILED: "Falha ao carregar TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Falha ao carregar EPUB"
|
||||
STR_SD_CARD_ERROR: "Erro no cartão SD"
|
||||
STR_WIFI_NETWORKS: "Redes Wi‑Fi"
|
||||
STR_NO_NETWORKS: "Sem redes"
|
||||
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||
STR_SCANNING: "Procurando..."
|
||||
STR_CONNECTING: "Conectando..."
|
||||
STR_CONNECTED: "Conectado!"
|
||||
STR_CONNECTION_FAILED: "Falha na conexão"
|
||||
STR_CONNECTION_TIMEOUT: "Tempo limite conexão"
|
||||
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
|
||||
STR_REMOVE_PASSWORD: "Remover senha salva?"
|
||||
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
|
||||
STR_PRESS_ANY_CONTINUE: "Pressione qualquer botão continuar"
|
||||
STR_SELECT_HINT: "ESQ/DIR: Escolher | OK: Confirmar"
|
||||
STR_HOW_CONNECT: "Como você gostaria se conectar?"
|
||||
STR_JOIN_NETWORK: "Entrar em uma rede"
|
||||
STR_CREATE_HOTSPOT: "Criar hotspot"
|
||||
STR_JOIN_DESC: "Conecte-se a uma rede Wi‑Fi existente"
|
||||
STR_HOTSPOT_DESC: "Crie uma rede Wi‑Fi outras pessoas entrarem"
|
||||
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
||||
STR_HOTSPOT_MODE: "Modo hotspot"
|
||||
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi‑Fi"
|
||||
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
|
||||
STR_OR_HTTP_PREFIX: "ou http://"
|
||||
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
||||
STR_CALIBRE_WEB_URL: "URL do Calibre Web"
|
||||
STR_CONNECT_WIRELESS: "Conectar como dispositivo sem fio"
|
||||
STR_NETWORK_LEGEND: "* = Criptografada | + = Salva"
|
||||
STR_MAC_ADDRESS: "Endereço MAC:"
|
||||
STR_CHECKING_WIFI: "Verificando Wi‑Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi‑Fi"
|
||||
STR_ENTER_TEXT: "Inserir texto"
|
||||
STR_TO_PREFIX: "para"
|
||||
STR_CALIBRE_DISCOVERING: "Procurando o Calibre..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Conectando a"
|
||||
STR_CALIBRE_CONNECTED_TO: "Conectado a"
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Aguardando comandos..."
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Falha conexão, tentando novamente)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Aguardando transferência..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "Se a transferência falhar, ative\n\\n'Ignorar espaço livre'\\n nas \\nconfigurações do\nplugin SmartDevice\\n Calibre."
|
||||
STR_CALIBRE_RECEIVING: "Recebendo:"
|
||||
STR_CALIBRE_RECEIVED: "Recebido:"
|
||||
STR_CALIBRE_WAITING_MORE: "Aguardando mais..."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Falha ao criar o arquivo"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Senha obrigatória"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transf. interrompida"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Esteja mesma rede Wi‑Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
|
||||
STR_CAT_DISPLAY: "Tela"
|
||||
STR_CAT_READER: "Leitor"
|
||||
STR_CAT_CONTROLS: "Controles"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
STR_SLEEP_SCREEN: "Tela de repouso"
|
||||
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
|
||||
STR_STATUS_BAR: "Barra de status"
|
||||
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
|
||||
STR_TEXT_AA: "Suavização de texto"
|
||||
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
||||
STR_ORIENTATION: "Orientação de leitura"
|
||||
STR_FRONT_BTN_LAYOUT: "Disposição botões frontais"
|
||||
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
||||
STR_LONG_PRESS_SKIP: "Pular capítulo com pressão longa"
|
||||
STR_FONT_FAMILY: "Fonte do leitor"
|
||||
STR_EXT_READER_FONT: "Fonte leitor externo"
|
||||
STR_EXT_CHINESE_FONT: "Fonte do leitor"
|
||||
STR_EXT_UI_FONT: "Fonte da interface"
|
||||
STR_FONT_SIZE: "Tam. fonte UI"
|
||||
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||
STR_ASCII_LETTER_SPACING: "Espaçamento letras ASCII"
|
||||
STR_ASCII_DIGIT_SPACING: "Espaçamento dígitos ASCII"
|
||||
STR_CJK_SPACING: "Espaçamento CJK"
|
||||
STR_COLOR_MODE: "Modo de cor"
|
||||
STR_SCREEN_MARGIN: "Margens da tela"
|
||||
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
|
||||
STR_HYPHENATION: "Hifenização"
|
||||
STR_TIME_TO_SLEEP: "Tempo para repousar"
|
||||
STR_REFRESH_FREQ: "Frequência atualização"
|
||||
STR_CALIBRE_SETTINGS: "Configuração do Calibre"
|
||||
STR_KOREADER_SYNC: "Sincronização KOReader"
|
||||
STR_CHECK_UPDATES: "Verificar atualizações"
|
||||
STR_LANGUAGE: "Idioma"
|
||||
STR_SELECT_WALLPAPER: "Escolher papel parede"
|
||||
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Nome de usuário"
|
||||
STR_PASSWORD: "Senha"
|
||||
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
||||
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
||||
STR_AUTHENTICATE: "Autenticar"
|
||||
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
||||
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
||||
STR_FILENAME: "Nome do arquivo"
|
||||
STR_BINARY: "Binário"
|
||||
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
|
||||
STR_WIFI_CONN_FAILED: "Falha na conexão Wi‑Fi"
|
||||
STR_AUTHENTICATING: "Autenticando..."
|
||||
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
|
||||
STR_KOREADER_AUTH: "Autenticação KOReader"
|
||||
STR_SYNC_READY: "A sincronização KOReader está pronta uso"
|
||||
STR_AUTH_FAILED: "Falha na autenticação"
|
||||
STR_DONE: "Feito"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Isso vai limpar todos os dados livros em cache."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Os livros precisarão ser reindexados"
|
||||
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
|
||||
STR_CLEARING_CACHE: "Limpando cache..."
|
||||
STR_CACHE_CLEARED: "Cache limpo"
|
||||
STR_ITEMS_REMOVED: "itens removidos"
|
||||
STR_FAILED_LOWER: "falhou"
|
||||
STR_CLEAR_CACHE_FAILED: "Falha ao limpar o cache"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Ver saída serial"
|
||||
STR_DARK: "Escuro"
|
||||
STR_LIGHT: "Claro"
|
||||
STR_CUSTOM: "Personalizado"
|
||||
STR_COVER: "Capa"
|
||||
STR_NONE_OPT: "Nenhum"
|
||||
STR_FIT: "Ajustar"
|
||||
STR_CROP: "Recortar"
|
||||
STR_NO_PROGRESS: "Sem progresso"
|
||||
STR_FULL_OPT: "Completo"
|
||||
STR_NEVER: "Nunca"
|
||||
STR_IN_READER: "No leitor"
|
||||
STR_ALWAYS: "Sempre"
|
||||
STR_IGNORE: "Ignorar"
|
||||
STR_SLEEP: "Repouso"
|
||||
STR_PAGE_TURN: "Virar página"
|
||||
STR_PORTRAIT: "Retrato"
|
||||
STR_LANDSCAPE_CW: "Paisagem H"
|
||||
STR_INVERTED: "Invertido"
|
||||
STR_LANDSCAPE_CCW: "Paisagem AH"
|
||||
STR_FRONT_LAYOUT_BCLR: "Vol, Conf, Esq, Dir"
|
||||
STR_FRONT_LAYOUT_LRBC: "Esq, Dir, Vol, Conf"
|
||||
STR_FRONT_LAYOUT_LBCR: "Esq, Vol, Conf, Dir"
|
||||
STR_PREV_NEXT: "Ant/Próx"
|
||||
STR_NEXT_PREV: "Próx/Ant"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Pequeno"
|
||||
STR_MEDIUM: "Médio"
|
||||
STR_LARGE: "Grande"
|
||||
STR_X_LARGE: "Extra grande"
|
||||
STR_TIGHT: "Apertado"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Largo"
|
||||
STR_JUSTIFY: "Justificar"
|
||||
STR_ALIGN_LEFT: "Esquerda"
|
||||
STR_CENTER: "Centralizar"
|
||||
STR_ALIGN_RIGHT: "Direita"
|
||||
STR_MIN_1: "1 min"
|
||||
STR_MIN_5: "5 min"
|
||||
STR_MIN_10: "10 min"
|
||||
STR_MIN_15: "15 min"
|
||||
STR_MIN_30: "30 min"
|
||||
STR_PAGES_1: "1 página"
|
||||
STR_PAGES_5: "5 páginas"
|
||||
STR_PAGES_10: "10 páginas"
|
||||
STR_PAGES_15: "15 páginas"
|
||||
STR_PAGES_30: "30 páginas"
|
||||
STR_UPDATE: "Atualizar"
|
||||
STR_CHECKING_UPDATE: "Verificando atualização..."
|
||||
STR_NEW_UPDATE: "Nova atualização disponível!"
|
||||
STR_CURRENT_VERSION: "Versão atual:"
|
||||
STR_NEW_VERSION: "Nova versão:"
|
||||
STR_UPDATING: "Atualizando..."
|
||||
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
||||
STR_UPDATE_FAILED: "Falha na atualização"
|
||||
STR_UPDATE_COMPLETE: "Atualização concluída"
|
||||
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente"
|
||||
STR_EXTERNAL_FONT: "Fonte externa"
|
||||
STR_BUILTIN_DISABLED: "Integrada (desativada)"
|
||||
STR_NO_ENTRIES: "Nenhum entries encontrado"
|
||||
STR_DOWNLOADING: "Baixando..."
|
||||
STR_DOWNLOAD_FAILED: "Falha no download"
|
||||
STR_ERROR_MSG: "Erro:"
|
||||
STR_UNNAMED: "Sem nome"
|
||||
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
|
||||
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
|
||||
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
|
||||
STR_NETWORK_PREFIX: "Rede:"
|
||||
STR_IP_ADDRESS_PREFIX: "Endereço IP:"
|
||||
STR_SCAN_QR_WIFI_HINT: "ou escaneie o QR code com seu celular conectar ao Wi‑Fi."
|
||||
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
|
||||
STR_SD_CARD: "Cartão SD"
|
||||
STR_BACK: "« Voltar"
|
||||
STR_EXIT: "« Sair"
|
||||
STR_HOME: "« Início"
|
||||
STR_SAVE: "« Salvar"
|
||||
STR_SELECT: "Escolher"
|
||||
STR_TOGGLE: "Alternar"
|
||||
STR_CONFIRM: "Confirmar"
|
||||
STR_CANCEL: "Cancelar"
|
||||
STR_CONNECT: "Conectar"
|
||||
STR_OPEN: "Abrir"
|
||||
STR_DOWNLOAD: "Baixar"
|
||||
STR_RETRY: "Tentar novamente"
|
||||
STR_YES: "Sim"
|
||||
STR_NO: "Não"
|
||||
STR_STATE_ON: "LIG."
|
||||
STR_STATE_OFF: "DESL."
|
||||
STR_SET: "Definir"
|
||||
STR_NOT_SET: "Não definido"
|
||||
STR_DIR_LEFT: "Esquerda"
|
||||
STR_DIR_RIGHT: "Direita"
|
||||
STR_DIR_UP: "Cima"
|
||||
STR_DIR_DOWN: "Baixo"
|
||||
STR_CAPS_ON: "CAPS"
|
||||
STR_CAPS_OFF: "caps"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[LIGADO]"
|
||||
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
|
||||
STR_FILTER_CONTRAST: "Contraste"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Completa c/ porcentagem"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Completa c/ barra livro"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Só barra do livro"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo"
|
||||
STR_UI_THEME: "Tema da interface"
|
||||
STR_THEME_CLASSIC: "Clássico"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
|
||||
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_COVER_CUSTOM: "Capa + personalizado"
|
||||
STR_RECENTS: "Recentes"
|
||||
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||
STR_NO_RECENT_BOOKS: "Sem livros recentes"
|
||||
STR_CALIBRE_DESC: "Usar transferências sem fio Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Esquecer a rede e remover a senha salva?"
|
||||
STR_FORGET_BUTTON: "Esquecer rede"
|
||||
STR_CALIBRE_STARTING: "Iniciando Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configuração"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Limpar"
|
||||
STR_DEFAULT_VALUE: "Padrão"
|
||||
STR_REMAP_PROMPT: "Pressione um botão frontal cada função"
|
||||
STR_UNASSIGNED: "Não atribuído"
|
||||
STR_ALREADY_ASSIGNED: "Já atribuído"
|
||||
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão"
|
||||
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento"
|
||||
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
||||
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
||||
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
|
||||
STR_GO_TO_PERCENT: "Ir para %"
|
||||
STR_GO_HOME_BUTTON: "Ir para o início"
|
||||
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||
STR_DELETE_CACHE: "Excluir cache do livro"
|
||||
STR_CHAPTER_PREFIX: "Capítulo:"
|
||||
STR_PAGES_SEPARATOR: "páginas |"
|
||||
STR_BOOK_PREFIX: "Livro:"
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "TRAVAR"
|
||||
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
||||
STR_PERCENT_STEP_HINT: "Esq/Dir: 1% Cima/Baixo: 10%"
|
||||
STR_SYNCING_TIME: "Sincronizando horário..."
|
||||
STR_CALC_HASH: "Calculando hash documento..."
|
||||
STR_HASH_FAILED: "Falha ao calcular o hash documento"
|
||||
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
|
||||
STR_UPLOAD_PROGRESS: "Enviando progresso..."
|
||||
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader em Config."
|
||||
STR_PROGRESS_FOUND: "Progresso encontrado!"
|
||||
STR_REMOTE_LABEL: "Remoto:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
|
||||
STR_DEVICE_FROM_FORMAT: "De: %s"
|
||||
STR_APPLY_REMOTE: "Aplicar progresso remoto"
|
||||
STR_UPLOAD_LOCAL: "Enviar progresso local"
|
||||
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
|
||||
STR_UPLOAD_PROMPT: "Enviar posição atual?"
|
||||
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
||||
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
||||
STR_SECTION_PREFIX: "Seção"
|
||||
STR_UPLOAD: "Enviar"
|
||||
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||
317
lib/I18n/translations/russia.yaml
Normal file
317
lib/I18n/translations/russia.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Русский"
|
||||
_language_code: "RUSSIAN"
|
||||
_order: "6"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "Загрузка"
|
||||
STR_SLEEPING: "Спящий режим"
|
||||
STR_ENTERING_SLEEP: "Переход в сон..."
|
||||
STR_BROWSE_FILES: "Обзор файлов"
|
||||
STR_FILE_TRANSFER: "Передача файлов"
|
||||
STR_SETTINGS_TITLE: "Настройки"
|
||||
STR_CALIBRE_LIBRARY: "Библиотека Calibre"
|
||||
STR_CONTINUE_READING: "Продолжить чтение"
|
||||
STR_NO_OPEN_BOOK: "Нет открытой книги"
|
||||
STR_START_READING: "Начать чтение ниже"
|
||||
STR_BOOKS: "Книги"
|
||||
STR_NO_BOOKS_FOUND: "Книги не найдены"
|
||||
STR_SELECT_CHAPTER: "Выберите главу"
|
||||
STR_NO_CHAPTERS: "Глав нет"
|
||||
STR_END_OF_BOOK: "Конец книги"
|
||||
STR_EMPTY_CHAPTER: "Пустая глава"
|
||||
STR_INDEXING: "Индексация..."
|
||||
STR_MEMORY_ERROR: "Ошибка памяти"
|
||||
STR_PAGE_LOAD_ERROR: "Ошибка загрузки страницы"
|
||||
STR_EMPTY_FILE: "Пустой файл"
|
||||
STR_OUT_OF_BOUNDS: "Выход за пределы"
|
||||
STR_LOADING: "Загрузка..."
|
||||
STR_LOAD_XTC_FAILED: "Не удалось загрузить XTC"
|
||||
STR_LOAD_TXT_FAILED: "Не удалось загрузить TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Не удалось загрузить EPUB"
|
||||
STR_SD_CARD_ERROR: "Ошибка SD-карты"
|
||||
STR_WIFI_NETWORKS: "Wi-Fi сети"
|
||||
STR_NO_NETWORKS: "Сети не найдены"
|
||||
STR_NETWORKS_FOUND: "Найдено сетей: %zu"
|
||||
STR_SCANNING: "Сканирование..."
|
||||
STR_CONNECTING: "Подключение..."
|
||||
STR_CONNECTED: "Подключено!"
|
||||
STR_CONNECTION_FAILED: "Ошибка подключения"
|
||||
STR_CONNECTION_TIMEOUT: "Тайм-аут подключения"
|
||||
STR_FORGET_NETWORK: "Забыть сеть?"
|
||||
STR_SAVE_PASSWORD: "Сохранить пароль?"
|
||||
STR_REMOVE_PASSWORD: "Удалить сохранённый пароль?"
|
||||
STR_PRESS_OK_SCAN: "Нажмите OK для повторного поиска"
|
||||
STR_PRESS_ANY_CONTINUE: "Нажмите любую кнопку"
|
||||
STR_SELECT_HINT: "ВЛЕВО/ВПРАВО: выбор | OK: подтвердить"
|
||||
STR_HOW_CONNECT: "Как вы хотите подключиться?"
|
||||
STR_JOIN_NETWORK: "Подключиться к сети"
|
||||
STR_CREATE_HOTSPOT: "Создать точку доступа"
|
||||
STR_JOIN_DESC: "Подключение к существующей сети Wi-Fi"
|
||||
STR_HOTSPOT_DESC: "Создать сеть Wi-Fi для подключения других"
|
||||
STR_STARTING_HOTSPOT: "Запуск точки доступа..."
|
||||
STR_HOTSPOT_MODE: "Режим точки доступа"
|
||||
STR_CONNECT_WIFI_HINT: "Подключите устройство к этой сети Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Откройте этот адрес в браузере"
|
||||
STR_OR_HTTP_PREFIX: "или http://"
|
||||
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
|
||||
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
|
||||
STR_CONNECT_WIRELESS: "Подключить как беспроводное устройство"
|
||||
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
|
||||
STR_MAC_ADDRESS: "MAC-адрес:"
|
||||
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Введите пароль Wi-Fi"
|
||||
STR_ENTER_TEXT: "Введите текст"
|
||||
STR_TO_PREFIX: "к "
|
||||
STR_CALIBRE_DISCOVERING: "Поиск Calibre..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Подключение к "
|
||||
STR_CALIBRE_CONNECTED_TO: "Подключено к "
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Ожидание команд..."
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Ошибка подключения"
|
||||
STR_CALIBRE_DISCONNECTED: "Соединение с Calibre разорвано"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Ожидание передачи..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "Если передача не удаётся"
|
||||
STR_CALIBRE_RECEIVING: "Получение:"
|
||||
STR_CALIBRE_RECEIVED: "Получено:"
|
||||
STR_CALIBRE_WAITING_MORE: "Ожидание следующих файлов..."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Не удалось создать файл"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Требуется пароль"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Передача прервана"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Установите плагин CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Подключитесь к той же сети Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) В Calibre выберите: «Отправить на устройство»"
|
||||
STR_CALIBRE_INSTRUCTION_4: "Не закрывайте этот экран во время отправки"
|
||||
STR_CAT_DISPLAY: "Экран"
|
||||
STR_CAT_READER: "Чтение"
|
||||
STR_CAT_CONTROLS: "Управление"
|
||||
STR_CAT_SYSTEM: "Система"
|
||||
STR_SLEEP_SCREEN: "Экран сна"
|
||||
STR_SLEEP_COVER_MODE: "Режим обложки сна"
|
||||
STR_STATUS_BAR: "Строка состояния"
|
||||
STR_HIDE_BATTERY: "Скрыть % батареи"
|
||||
STR_EXTRA_SPACING: "Доп. интервал абзаца"
|
||||
STR_TEXT_AA: "Сглаживание текста"
|
||||
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
|
||||
STR_ORIENTATION: "Ориентация чтения"
|
||||
STR_FRONT_BTN_LAYOUT: "Боковые кнопки"
|
||||
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
|
||||
STR_LONG_PRESS_SKIP: "Долгое нажатие - смена главы"
|
||||
STR_FONT_FAMILY: "Шрифт чтения"
|
||||
STR_EXT_READER_FONT: "Внешний шрифт чтения"
|
||||
STR_EXT_CHINESE_FONT: "Шрифт CJK"
|
||||
STR_EXT_UI_FONT: "Шрифт интерфейса"
|
||||
STR_FONT_SIZE: "Размер шрифта интерфейса"
|
||||
STR_LINE_SPACING: "Межстрочный интервал"
|
||||
STR_ASCII_LETTER_SPACING: "Интервал букв ASCII"
|
||||
STR_ASCII_DIGIT_SPACING: "Интервал цифр ASCII"
|
||||
STR_CJK_SPACING: "Интервал CJK"
|
||||
STR_COLOR_MODE: "Цветовой режим"
|
||||
STR_SCREEN_MARGIN: "Поля экрана"
|
||||
STR_PARA_ALIGNMENT: "Выравнивание абзаца"
|
||||
STR_HYPHENATION: "Перенос слов"
|
||||
STR_TIME_TO_SLEEP: "Сон Через"
|
||||
STR_REFRESH_FREQ: "Частота обновления"
|
||||
STR_CALIBRE_SETTINGS: "Настройки Calibre"
|
||||
STR_KOREADER_SYNC: "Синхронизация KOReader"
|
||||
STR_CHECK_UPDATES: "Проверить обновления"
|
||||
STR_LANGUAGE: "Язык"
|
||||
STR_SELECT_WALLPAPER: "Выбрать обои"
|
||||
STR_CLEAR_READING_CACHE: "Очистить кэш чтения"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Имя пользователя"
|
||||
STR_PASSWORD: "Пароль"
|
||||
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
||||
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
||||
STR_AUTHENTICATE: "Авторизация"
|
||||
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
STR_FILENAME: "Имя файла"
|
||||
STR_BINARY: "Бинарный"
|
||||
STR_SET_CREDENTIALS_FIRST: "Сначала укажите данные"
|
||||
STR_WIFI_CONN_FAILED: "Не удалось подключиться к Wi-Fi"
|
||||
STR_AUTHENTICATING: "Авторизация..."
|
||||
STR_AUTH_SUCCESS: "Авторизация успешна!"
|
||||
STR_KOREADER_AUTH: "Авторизация KOReader"
|
||||
STR_SYNC_READY: "Синхронизация KOReader готова"
|
||||
STR_AUTH_FAILED: "Ошибка авторизации"
|
||||
STR_DONE: "Готово"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Будут удалены все данные кэша книг."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Весь прогресс чтения будет потерян!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Книги потребуется переиндексировать"
|
||||
STR_CLEAR_CACHE_WARNING_4: "при повторном открытии."
|
||||
STR_CLEARING_CACHE: "Очистка кэша..."
|
||||
STR_CACHE_CLEARED: "Кэш очищен"
|
||||
STR_ITEMS_REMOVED: "элементов удалено"
|
||||
STR_FAILED_LOWER: "ошибка"
|
||||
STR_CLEAR_CACHE_FAILED: "Не удалось очистить кэш"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Проверьте вывод по UART для деталей"
|
||||
STR_DARK: "Тёмный"
|
||||
STR_LIGHT: "Светлый"
|
||||
STR_CUSTOM: "Свой"
|
||||
STR_COVER: "Обложка"
|
||||
STR_NONE_OPT: "Нет"
|
||||
STR_FIT: "Вписать"
|
||||
STR_CROP: "Обрезать"
|
||||
STR_NO_PROGRESS: "Без прогресса"
|
||||
STR_FULL_OPT: "Полная"
|
||||
STR_NEVER: "Никогда"
|
||||
STR_IN_READER: "В режиме чтения"
|
||||
STR_ALWAYS: "Всегда"
|
||||
STR_IGNORE: "Игнорировать"
|
||||
STR_SLEEP: "Сон"
|
||||
STR_PAGE_TURN: "Перелистывание"
|
||||
STR_PORTRAIT: "Портрет"
|
||||
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||
STR_INVERTED: "Инверсия"
|
||||
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||
STR_FRONT_LAYOUT_BCLR: "Наз"
|
||||
STR_FRONT_LAYOUT_LRBC: "Лев"
|
||||
STR_FRONT_LAYOUT_LBCR: "Лев"
|
||||
STR_PREV_NEXT: "Назад/Вперёд"
|
||||
STR_NEXT_PREV: "Вперёд/Назад"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Маленький"
|
||||
STR_MEDIUM: "Средний"
|
||||
STR_LARGE: "Большой"
|
||||
STR_X_LARGE: "Очень большой"
|
||||
STR_TIGHT: "Узкий"
|
||||
STR_NORMAL: "Обычный"
|
||||
STR_WIDE: "Широкий"
|
||||
STR_JUSTIFY: "По ширине"
|
||||
STR_ALIGN_LEFT: "По левому краю"
|
||||
STR_CENTER: "По центру"
|
||||
STR_ALIGN_RIGHT: "По правому краю"
|
||||
STR_MIN_1: "1 мин"
|
||||
STR_MIN_5: "5 мин"
|
||||
STR_MIN_10: "10 мин"
|
||||
STR_MIN_15: "15 мин"
|
||||
STR_MIN_30: "30 мин"
|
||||
STR_PAGES_1: "1 стр."
|
||||
STR_PAGES_5: "5 стр."
|
||||
STR_PAGES_10: "10 стр."
|
||||
STR_PAGES_15: "15 стр."
|
||||
STR_PAGES_30: "30 стр."
|
||||
STR_UPDATE: "Обновление"
|
||||
STR_CHECKING_UPDATE: "Проверка обновлений..."
|
||||
STR_NEW_UPDATE: "Доступно новое обновление!"
|
||||
STR_CURRENT_VERSION: "Текущая версия:"
|
||||
STR_NEW_VERSION: "Новая версия:"
|
||||
STR_UPDATING: "Обновление..."
|
||||
STR_NO_UPDATE: "Обновлений нет"
|
||||
STR_UPDATE_FAILED: "Ошибка обновления"
|
||||
STR_UPDATE_COMPLETE: "Обновление завершено"
|
||||
STR_POWER_ON_HINT: "Удерживайте кнопку питания для включения"
|
||||
STR_EXTERNAL_FONT: "Внешний шрифт"
|
||||
STR_BUILTIN_DISABLED: "Встроенный (отключён)"
|
||||
STR_NO_ENTRIES: "Записи не найдены"
|
||||
STR_DOWNLOADING: "Загрузка..."
|
||||
STR_DOWNLOAD_FAILED: "Ошибка загрузки"
|
||||
STR_ERROR_MSG: "Ошибка:"
|
||||
STR_UNNAMED: "Без имени"
|
||||
STR_NO_SERVER_URL: "URL сервера не настроен"
|
||||
STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
|
||||
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
|
||||
STR_NETWORK_PREFIX: "Сеть:"
|
||||
STR_IP_ADDRESS_PREFIX: "IP-адрес:"
|
||||
STR_SCAN_QR_WIFI_HINT: "или отсканируйте QR-код для подключения к Wi-Fi."
|
||||
STR_ERROR_GENERAL_FAILURE: "Ошибка: Общая ошибка"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Ошибка: Сеть не найдена"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Ошибка: Тайм-аут соединения"
|
||||
STR_SD_CARD: "SD-карта"
|
||||
STR_BACK: "« Назад"
|
||||
STR_EXIT: "« Выход"
|
||||
STR_HOME: "« Главная"
|
||||
STR_SAVE: "« Сохранить"
|
||||
STR_SELECT: "Выбрать"
|
||||
STR_TOGGLE: "Выбор"
|
||||
STR_CONFIRM: "Подтв."
|
||||
STR_CANCEL: "Отмена"
|
||||
STR_CONNECT: "Подкл."
|
||||
STR_OPEN: "Открыть"
|
||||
STR_DOWNLOAD: "Скачать"
|
||||
STR_RETRY: "Повторить"
|
||||
STR_YES: "Да"
|
||||
STR_NO: "Нет"
|
||||
STR_STATE_ON: "ВКЛ"
|
||||
STR_STATE_OFF: "ВЫКЛ"
|
||||
STR_SET: "Установлено"
|
||||
STR_NOT_SET: "Не установлено"
|
||||
STR_DIR_LEFT: "Влево"
|
||||
STR_DIR_RIGHT: "Вправо"
|
||||
STR_DIR_UP: "Вверх"
|
||||
STR_DIR_DOWN: "Вниз"
|
||||
STR_CAPS_ON: "CAPS"
|
||||
STR_CAPS_OFF: "caps"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[ВКЛ]"
|
||||
STR_SLEEP_COVER_FILTER: "Фильтр обложки сна"
|
||||
STR_FILTER_CONTRAST: "Контраст"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Полная + %"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Полная + шкала книги"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Только шкала книги"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы"
|
||||
STR_UI_THEME: "Тема интерфейса"
|
||||
STR_THEME_CLASSIC: "Классическая"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
|
||||
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
|
||||
STR_OPDS_BROWSER: "OPDS браузер"
|
||||
STR_COVER_CUSTOM: "Обложка + Свой"
|
||||
STR_RECENTS: "Недавние"
|
||||
STR_MENU_RECENT_BOOKS: "Недавние книги"
|
||||
STR_NO_RECENT_BOOKS: "Нет недавних книг"
|
||||
STR_CALIBRE_DESC: "Использовать беспроводную передачу Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Забыть сеть и удалить сохранённый пароль?"
|
||||
STR_FORGET_BUTTON: "Забыть сеть"
|
||||
STR_CALIBRE_STARTING: "Запуск Calibre..."
|
||||
STR_CALIBRE_SETUP: "Настройка"
|
||||
STR_CALIBRE_STATUS: "Статус"
|
||||
STR_CLEAR_BUTTON: "Очистить"
|
||||
STR_DEFAULT_VALUE: "По умолчанию"
|
||||
STR_REMAP_PROMPT: "Назначьте роль для каждой кнопки"
|
||||
STR_UNASSIGNED: "Не назначено"
|
||||
STR_ALREADY_ASSIGNED: "Уже назначено"
|
||||
STR_REMAP_RESET_HINT: "Боковая кнопка вверх: сбросить по умолчанию"
|
||||
STR_REMAP_CANCEL_HINT: "Боковая кнопка вниз: отменить переназначение"
|
||||
STR_HW_BACK_LABEL: "Назад (1-я кнопка)"
|
||||
STR_HW_CONFIRM_LABEL: "Подтвердить (2-я кнопка)"
|
||||
STR_HW_LEFT_LABEL: "Влево (3-я кнопка)"
|
||||
STR_HW_RIGHT_LABEL: "Вправо (4-я кнопка)"
|
||||
STR_GO_TO_PERCENT: "Перейти к %"
|
||||
STR_GO_HOME_BUTTON: "На главную"
|
||||
STR_SYNC_PROGRESS: "Синхронизировать прогресс"
|
||||
STR_DELETE_CACHE: "Удалить кэш книги"
|
||||
STR_CHAPTER_PREFIX: "Глава:"
|
||||
STR_PAGES_SEPARATOR: "стр. |"
|
||||
STR_BOOK_PREFIX: "Книга:"
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "LOCK"
|
||||
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
|
||||
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
|
||||
STR_SYNCING_TIME: "Синхронизация времени..."
|
||||
STR_CALC_HASH: "Расчёт хэша документа..."
|
||||
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
|
||||
STR_FETCH_PROGRESS: "Получение удалённого прогресса..."
|
||||
STR_UPLOAD_PROGRESS: "Отправка прогресса..."
|
||||
STR_NO_CREDENTIALS_MSG: "Данные для входа не настроены"
|
||||
STR_KOREADER_SETUP_HINT: "Настройте аккаунт KOReader в настройках"
|
||||
STR_PROGRESS_FOUND: "Прогресс найден!"
|
||||
STR_REMOTE_LABEL: "Удалённый:"
|
||||
STR_LOCAL_LABEL: "Локальный:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Страница %d, %.2f%% всего"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Страница %d/%d"
|
||||
STR_DEVICE_FROM_FORMAT: "От: %s"
|
||||
STR_APPLY_REMOTE: "Применить удалённый прогресс"
|
||||
STR_UPLOAD_LOCAL: "Отправить локальный прогресс"
|
||||
STR_NO_REMOTE_MSG: "Удалённый прогресс не найден"
|
||||
STR_UPLOAD_PROMPT: "Отправить текущую позицию?"
|
||||
STR_UPLOAD_SUCCESS: "Прогресс отправлен!"
|
||||
STR_SYNC_FAILED_MSG: "Ошибка синхронизации"
|
||||
STR_SECTION_PREFIX: "Раздел"
|
||||
STR_UPLOAD: "Отправить"
|
||||
STR_BOOK_S_STYLE: "Стиль книги"
|
||||
STR_EMBEDDED_STYLE: "Встроенный стиль"
|
||||
STR_OPDS_SERVER_URL: "URL OPDS сервера"
|
||||
317
lib/I18n/translations/spanish.yaml
Normal file
317
lib/I18n/translations/spanish.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Español"
|
||||
_language_code: "SPANISH"
|
||||
_order: "1"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "BOOTING"
|
||||
STR_SLEEPING: "SLEEPING"
|
||||
STR_ENTERING_SLEEP: "ENTERING SLEEP..."
|
||||
STR_BROWSE_FILES: "Buscar archivos"
|
||||
STR_FILE_TRANSFER: "Transferencia de archivos"
|
||||
STR_SETTINGS_TITLE: "Configuración"
|
||||
STR_CALIBRE_LIBRARY: "Libreria Calibre"
|
||||
STR_CONTINUE_READING: "Continuar leyendo"
|
||||
STR_NO_OPEN_BOOK: "No hay libros abiertos"
|
||||
STR_START_READING: "Start reading below"
|
||||
STR_BOOKS: "Libros"
|
||||
STR_NO_BOOKS_FOUND: "No se encontraron libros"
|
||||
STR_SELECT_CHAPTER: "Seleccionar capítulo"
|
||||
STR_NO_CHAPTERS: "Sin capítulos"
|
||||
STR_END_OF_BOOK: "Fin del libro"
|
||||
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
||||
STR_INDEXING: "Indexando..."
|
||||
STR_MEMORY_ERROR: "Error de memoria"
|
||||
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
||||
STR_EMPTY_FILE: "Archivo vacío"
|
||||
STR_OUT_OF_BOUNDS: "Out of bounds"
|
||||
STR_LOADING: "Cargando..."
|
||||
STR_LOAD_XTC_FAILED: "Error al cargar XTC"
|
||||
STR_LOAD_TXT_FAILED: "Error al cargar TXT"
|
||||
STR_LOAD_EPUB_FAILED: "Error al cargar EPUB"
|
||||
STR_SD_CARD_ERROR: "Error en la tarjeta SD"
|
||||
STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No hay redes disponibles"
|
||||
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||
STR_SCANNING: "Buscando..."
|
||||
STR_CONNECTING: "Conectando..."
|
||||
STR_CONNECTED: "Conectado!"
|
||||
STR_CONNECTION_FAILED: "Error de conexion"
|
||||
STR_CONNECTION_TIMEOUT: "Connection timeout"
|
||||
STR_FORGET_NETWORK: "Olvidar la red?"
|
||||
STR_SAVE_PASSWORD: "Guardar contraseña para la próxima vez?"
|
||||
STR_REMOVE_PASSWORD: "Borrar contraseñas guardadas?"
|
||||
STR_PRESS_OK_SCAN: "Presione OK para buscar de nuevo"
|
||||
STR_PRESS_ANY_CONTINUE: "Presione cualquier botón para continuar"
|
||||
STR_SELECT_HINT: "Izquierda/Derecha: Seleccionar | OK: Confirmar"
|
||||
STR_HOW_CONNECT: "Cómo te gustaría conectarte?"
|
||||
STR_JOIN_NETWORK: "Unirse a una red"
|
||||
STR_CREATE_HOTSPOT: "Crear punto de acceso"
|
||||
STR_JOIN_DESC: "Conectarse a una red Wi-Fi existente"
|
||||
STR_HOTSPOT_DESC: "Crear una red Wi-Fi para que otros se unan"
|
||||
STR_STARTING_HOTSPOT: "Iniciando punto de acceso..."
|
||||
STR_HOTSPOT_MODE: "Modo punto de acceso"
|
||||
STR_CONNECT_WIFI_HINT: "Conectar su dispositivo a esta red Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Abre esta dirección en tu navegador"
|
||||
STR_OR_HTTP_PREFIX: "o http://"
|
||||
STR_SCAN_QR_HINT: "o escanee este código QR con su móvil:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre inalámbrico"
|
||||
STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre"
|
||||
STR_CONNECT_WIRELESS: "Conectar como dispositivo inalámbrico"
|
||||
STR_NETWORK_LEGEND: "* = Cifrado | + = Guardado"
|
||||
STR_MAC_ADDRESS: "Dirección MAC:"
|
||||
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Introduzca la contraseña de Wi-Fi"
|
||||
STR_ENTER_TEXT: "Introduzca el texto"
|
||||
STR_TO_PREFIX: "a "
|
||||
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Conectándose a"
|
||||
STR_CALIBRE_CONNECTED_TO: "Conectado a "
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Esperando comandos..."
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Error de conexión, intentándolo nuevamente)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Esperando transferencia..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "Si la transferencia falla, habilite \\n'Ignorar espacio libre' en las configuraciones del \\nplugin smartdevice de calibre."
|
||||
STR_CALIBRE_RECEIVING: "Recibiendo: "
|
||||
STR_CALIBRE_RECEIVED: "Recibido: "
|
||||
STR_CALIBRE_WAITING_MORE: "Esperando más..."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Error al crear el archivo"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Contraseña requerida"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transferencia interrumpida"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instala CrossPoint Reader plugin"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Conéctese a la misma red Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) En Calibre: \"Enviar a dispotivo\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Permanezca en esta pantalla mientras se envía\""
|
||||
STR_CAT_DISPLAY: "Pantalla"
|
||||
STR_CAT_READER: "Lector"
|
||||
STR_CAT_CONTROLS: "Control"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
STR_SLEEP_SCREEN: "Salva Pantallas"
|
||||
STR_SLEEP_COVER_MODE: "Modo de salva pantallas"
|
||||
STR_STATUS_BAR: "Barra de estado"
|
||||
STR_HIDE_BATTERY: "Ocultar porcentaje de batería"
|
||||
STR_EXTRA_SPACING: "Espaciado extra de párrafos"
|
||||
STR_TEXT_AA: "Suavizado de bordes de texto"
|
||||
STR_SHORT_PWR_BTN: "Clic breve del botón de encendido"
|
||||
STR_ORIENTATION: "Orientación de la lectura"
|
||||
STR_FRONT_BTN_LAYOUT: "Diseño de los botones frontales"
|
||||
STR_SIDE_BTN_LAYOUT: "Diseño de los botones laterales (Lector)"
|
||||
STR_LONG_PRESS_SKIP: "Pasar a la capítulo al presiónar largamente"
|
||||
STR_FONT_FAMILY: "Familia de tipografía del lector"
|
||||
STR_EXT_READER_FONT: "Tipografía externa"
|
||||
STR_EXT_CHINESE_FONT: "Tipografía (Lectura)"
|
||||
STR_EXT_UI_FONT: "Tipografía (Pantalla)"
|
||||
STR_FONT_SIZE: "Tamaño de la fuente (Pantalla)"
|
||||
STR_LINE_SPACING: "Interlineado (Lectura)"
|
||||
STR_ASCII_LETTER_SPACING: "Espaciado de letras ASCII"
|
||||
STR_ASCII_DIGIT_SPACING: "Espaciado de dígitos ASCII"
|
||||
STR_CJK_SPACING: "Espaciado CJK"
|
||||
STR_COLOR_MODE: "Modo de color"
|
||||
STR_SCREEN_MARGIN: "Margen de lectura"
|
||||
STR_PARA_ALIGNMENT: "Ajuste de parágrafo del lector"
|
||||
STR_HYPHENATION: "Hyphenation"
|
||||
STR_TIME_TO_SLEEP: "Tiempo para dormir"
|
||||
STR_REFRESH_FREQ: "Frecuencia de actualización"
|
||||
STR_CALIBRE_SETTINGS: "Configuraciones de Calibre"
|
||||
STR_KOREADER_SYNC: "Síncronización de KOReader"
|
||||
STR_CHECK_UPDATES: "Verificar actualizaciones"
|
||||
STR_LANGUAGE: "Idioma"
|
||||
STR_SELECT_WALLPAPER: "Seleccionar fondo"
|
||||
STR_CLEAR_READING_CACHE: "Borrar caché de lectura"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Nombre de usuario"
|
||||
STR_PASSWORD: "Contraseña"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de síncronización"
|
||||
STR_DOCUMENT_MATCHING: "Coincidencia de documentos"
|
||||
STR_AUTHENTICATE: "Autentificar"
|
||||
STR_KOREADER_USERNAME: "Nombre de usuario de KOReader"
|
||||
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
||||
STR_FILENAME: "Nombre del archivo"
|
||||
STR_BINARY: "Binario"
|
||||
STR_SET_CREDENTIALS_FIRST: "Configurar credenciales primero"
|
||||
STR_WIFI_CONN_FAILED: "Falló la conexión Wi-Fi"
|
||||
STR_AUTHENTICATING: "Autentificando..."
|
||||
STR_AUTH_SUCCESS: "Autenticación exitsosa!"
|
||||
STR_KOREADER_AUTH: "Autenticación KOReader"
|
||||
STR_SYNC_READY: "La síncronización de KOReader está lista para usarse"
|
||||
STR_AUTH_FAILED: "Falló la autenticación"
|
||||
STR_DONE: "Hecho"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Esto borrará todos los datos en cache del libro."
|
||||
STR_CLEAR_CACHE_WARNING_2: " ¡Se perderá todo el avance de leer!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Los libros deberán ser reíndexados"
|
||||
STR_CLEAR_CACHE_WARNING_4: "cuando se abran de nuevo."
|
||||
STR_CLEARING_CACHE: "Borrando caché..."
|
||||
STR_CACHE_CLEARED: "Cache limpia"
|
||||
STR_ITEMS_REMOVED: "Elementos eliminados"
|
||||
STR_FAILED_LOWER: "Falló"
|
||||
STR_CLEAR_CACHE_FAILED: "No se pudo borrar la cache"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Verifique la salida serial para detalles"
|
||||
STR_DARK: "Oscuro"
|
||||
STR_LIGHT: "Claro"
|
||||
STR_CUSTOM: "Personalizado"
|
||||
STR_COVER: "Portada"
|
||||
STR_NONE_OPT: "Ninguno"
|
||||
STR_FIT: "Ajustar"
|
||||
STR_CROP: "Recortar"
|
||||
STR_NO_PROGRESS: "Sin avance"
|
||||
STR_FULL_OPT: "Completa"
|
||||
STR_NEVER: "Nunca"
|
||||
STR_IN_READER: "En el lector"
|
||||
STR_ALWAYS: "Siempre"
|
||||
STR_IGNORE: "Ignorar"
|
||||
STR_SLEEP: "Dormir"
|
||||
STR_PAGE_TURN: "Paso de página"
|
||||
STR_PORTRAIT: "Portrato"
|
||||
STR_LANDSCAPE_CW: "Paisaje sentido horario"
|
||||
STR_INVERTED: "Invertido"
|
||||
STR_LANDSCAPE_CCW: "Paisaje sentido antihorario"
|
||||
STR_FRONT_LAYOUT_BCLR: "Atrás, Confirmar, Izquierda, Derecha"
|
||||
STR_FRONT_LAYOUT_LRBC: "Izquierda, Derecha, Atrás, Confirmar"
|
||||
STR_FRONT_LAYOUT_LBCR: "Izquierda, Atrás, Confirmar, Derecha"
|
||||
STR_PREV_NEXT: "Anterior/Siguiente"
|
||||
STR_NEXT_PREV: "Siguiente/Anterior"
|
||||
STR_BOOKERLY: "Relacionado con libros"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Pequeño"
|
||||
STR_MEDIUM: "Medio"
|
||||
STR_LARGE: "Grande"
|
||||
STR_X_LARGE: "Extra grande"
|
||||
STR_TIGHT: "Ajustado"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Ancho"
|
||||
STR_JUSTIFY: "Justificar"
|
||||
STR_ALIGN_LEFT: "Izquierda"
|
||||
STR_CENTER: "Centro"
|
||||
STR_ALIGN_RIGHT: "Derecha"
|
||||
STR_MIN_1: "1 Minuto"
|
||||
STR_MIN_5: "10 Minutos"
|
||||
STR_MIN_10: "5 Minutos"
|
||||
STR_MIN_15: "15 Minutos"
|
||||
STR_MIN_30: "30 Minutos"
|
||||
STR_PAGES_1: "1 Página"
|
||||
STR_PAGES_5: "5 Páginas"
|
||||
STR_PAGES_10: "10 Páginas"
|
||||
STR_PAGES_15: "15 Páginas"
|
||||
STR_PAGES_30: "30 Páginas"
|
||||
STR_UPDATE: "ActualizaR"
|
||||
STR_CHECKING_UPDATE: "Verificando actualización..."
|
||||
STR_NEW_UPDATE: "¡Nueva actualización disponible!"
|
||||
STR_CURRENT_VERSION: "Versión actual:"
|
||||
STR_NEW_VERSION: "Nueva versión:"
|
||||
STR_UPDATING: "Actualizando..."
|
||||
STR_NO_UPDATE: "No hay actualizaciones disponibles"
|
||||
STR_UPDATE_FAILED: "Falló la actualización"
|
||||
STR_UPDATE_COMPLETE: "Actualización completada"
|
||||
STR_POWER_ON_HINT: "Presione y mantenga presionado el botón de encendido para volver a encender"
|
||||
STR_EXTERNAL_FONT: "Fuente externa"
|
||||
STR_BUILTIN_DISABLED: "Incorporado (Desactivado)"
|
||||
STR_NO_ENTRIES: "No se encontraron elementos"
|
||||
STR_DOWNLOADING: "Descargando..."
|
||||
STR_DOWNLOAD_FAILED: "Falló la descarga"
|
||||
STR_ERROR_MSG: "Error"
|
||||
STR_UNNAMED: "Sin nombre"
|
||||
STR_NO_SERVER_URL: "No se ha configurado la url del servidor"
|
||||
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||
STR_NETWORK_PREFIX: "Red: "
|
||||
STR_IP_ADDRESS_PREFIX: "Dirección IP: "
|
||||
STR_SCAN_QR_WIFI_HINT: "O escanee el código QR con su teléfono para conectarse a WI-FI."
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallo general"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Error: Red no encontrada"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
|
||||
STR_SD_CARD: "Tarjeta SD"
|
||||
STR_BACK: "« Atrás"
|
||||
STR_EXIT: "« SaliR"
|
||||
STR_HOME: "« Inicio"
|
||||
STR_SAVE: "« Guardar"
|
||||
STR_SELECT: "Seleccionar"
|
||||
STR_TOGGLE: "Cambiar"
|
||||
STR_CONFIRM: "Confirmar"
|
||||
STR_CANCEL: "Cancelar"
|
||||
STR_CONNECT: "Conectar"
|
||||
STR_OPEN: "Abrir"
|
||||
STR_DOWNLOAD: "Descargar"
|
||||
STR_RETRY: "Reintentar"
|
||||
STR_YES: "Sí"
|
||||
STR_NO: "No"
|
||||
STR_STATE_ON: "ENCENDIDO"
|
||||
STR_STATE_OFF: "APAGADO"
|
||||
STR_SET: "Configurar"
|
||||
STR_NOT_SET: "No configurado"
|
||||
STR_DIR_LEFT: "Izquierda"
|
||||
STR_DIR_RIGHT: "Derecha"
|
||||
STR_DIR_UP: "Arriba"
|
||||
STR_DIR_DOWN: "Abajo"
|
||||
STR_CAPS_ON: "MAYÚSCULAS"
|
||||
STR_CAPS_OFF: "caps"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_ON_MARKER: "[ENCENDIDO]"
|
||||
STR_SLEEP_COVER_FILTER: "Filtro de salva pantalla y protección de la pantalla"
|
||||
STR_FILTER_CONTRAST: "Contraste"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Completa con porcentaje"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Completa con progreso del libro"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos"
|
||||
STR_UI_THEME: "Estilo de pantalla"
|
||||
STR_THEME_CLASSIC: "Clásico"
|
||||
STR_THEME_LYRA: "LYRA"
|
||||
STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
|
||||
STR_OPDS_BROWSER: "Navegador opds"
|
||||
STR_COVER_CUSTOM: "Portada + Personalizado"
|
||||
STR_RECENTS: "Recientes"
|
||||
STR_MENU_RECENT_BOOKS: "Libros recientes"
|
||||
STR_NO_RECENT_BOOKS: "No hay libros recientes"
|
||||
STR_CALIBRE_DESC: "Utilice las transferencias dispositivos inalámbricos de calibre"
|
||||
STR_FORGET_AND_REMOVE: "Olvidar la red y eliminar la contraseña guardada?"
|
||||
STR_FORGET_BUTTON: "Olvidar la red"
|
||||
STR_CALIBRE_STARTING: "Iniciando calibre..."
|
||||
STR_CALIBRE_SETUP: "Configuración"
|
||||
STR_CALIBRE_STATUS: "Estado"
|
||||
STR_CLEAR_BUTTON: "Borrar"
|
||||
STR_DEFAULT_VALUE: "Previo"
|
||||
STR_REMAP_PROMPT: "Presione un botón frontal para cada función"
|
||||
STR_UNASSIGNED: "No asignado"
|
||||
STR_ALREADY_ASSIGNED: "Ya asignado"
|
||||
STR_REMAP_RESET_HINT: "Botón lateral arriba: Restablecer a la configuración previo"
|
||||
STR_REMAP_CANCEL_HINT: "Botón lateral abajo: Anular reconfiguración"
|
||||
STR_HW_BACK_LABEL: "Atrás (Primer botón)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirmar (Segundo botón)"
|
||||
STR_HW_LEFT_LABEL: "Izquierda (Tercer botón)"
|
||||
STR_HW_RIGHT_LABEL: "Derecha (Cuarto botón)"
|
||||
STR_GO_TO_PERCENT: "Ir a %"
|
||||
STR_GO_HOME_BUTTON: "Volver a inicio"
|
||||
STR_SYNC_PROGRESS: "Progreso de síncronización"
|
||||
STR_DELETE_CACHE: "Borrar cache del libro"
|
||||
STR_CHAPTER_PREFIX: "Capítulo:"
|
||||
STR_PAGES_SEPARATOR: " Páginas |"
|
||||
STR_BOOK_PREFIX: "Libro:"
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "BLOQUEAR"
|
||||
STR_CALIBRE_URL_HINT: "Para calibre, agregue /opds a su urL"
|
||||
STR_PERCENT_STEP_HINT: "Izquierda/Derecha: 1% Arriba/Abajo: 10%"
|
||||
STR_SYNCING_TIME: "Tiempo de síncronización..."
|
||||
STR_CALC_HASH: "Calculando hash del documento..."
|
||||
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
||||
STR_FETCH_PROGRESS: "Recuperando progreso remoto..."
|
||||
STR_UPLOAD_PROGRESS: "Subiendo progreso..."
|
||||
STR_NO_CREDENTIALS_MSG: "No se han configurado credenciales"
|
||||
STR_KOREADER_SETUP_HINT: "Configure una cuenta de KOReader en la configuración"
|
||||
STR_PROGRESS_FOUND: "¡Progreso encontrado!"
|
||||
STR_REMOTE_LABEL: "Remoto"
|
||||
STR_LOCAL_LABEL: "Local"
|
||||
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% Completada"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d / %d, %.2f% Completada"
|
||||
STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||
STR_APPLY_REMOTE: "Aplicar progreso remoto"
|
||||
STR_UPLOAD_LOCAL: "Subir progreso local"
|
||||
STR_NO_REMOTE_MSG: "No se encontró progreso remoto"
|
||||
STR_UPLOAD_PROMPT: "Subir posicion actual?"
|
||||
STR_UPLOAD_SUCCESS: "¡Progreso subido!"
|
||||
STR_SYNC_FAILED_MSG: "Fallo de síncronización"
|
||||
STR_SECTION_PREFIX: "Seccion"
|
||||
STR_UPLOAD: "Subir"
|
||||
STR_BOOK_S_STYLE: "Estilo del libro"
|
||||
STR_EMBEDDED_STYLE: "Estilo integrado"
|
||||
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||
317
lib/I18n/translations/swedish.yaml
Normal file
317
lib/I18n/translations/swedish.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_language_name: "Svenska"
|
||||
_language_code: "SWEDISH"
|
||||
_order: "7"
|
||||
|
||||
STR_CROSSPOINT: "Crosspoint"
|
||||
STR_BOOTING: "STARTAR"
|
||||
STR_SLEEPING: "VILA"
|
||||
STR_ENTERING_SLEEP: "Går i vila…"
|
||||
STR_BROWSE_FILES: "Bläddra filer…"
|
||||
STR_FILE_TRANSFER: "Filöverföring"
|
||||
STR_SETTINGS_TITLE: "Inställningar"
|
||||
STR_CALIBRE_LIBRARY: "Calibrebibliotek"
|
||||
STR_CONTINUE_READING: "Fortsätt läsa"
|
||||
STR_NO_OPEN_BOOK: "Ingen öppen bok"
|
||||
STR_START_READING: "Börja läsa nedan"
|
||||
STR_BOOKS: "Böcker"
|
||||
STR_NO_BOOKS_FOUND: "Inga böcker hittade"
|
||||
STR_SELECT_CHAPTER: "Välj kapitel"
|
||||
STR_NO_CHAPTERS: "Inga kapitel"
|
||||
STR_END_OF_BOOK: "Slutet på boken"
|
||||
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
||||
STR_INDEXING: "Indexerar…"
|
||||
STR_MEMORY_ERROR: "Minnesfel"
|
||||
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
||||
STR_EMPTY_FILE: "Tom fil"
|
||||
STR_OUT_OF_BOUNDS: "Utanför gränserna"
|
||||
STR_LOADING: "Laddar…"
|
||||
STR_LOAD_XTC_FAILED: "Misslyckades ladda XTC"
|
||||
STR_LOAD_TXT_FAILED: "Misslyckades ladda TCT"
|
||||
STR_LOAD_EPUB_FAILED: "Misslyckades ladda EPUB"
|
||||
STR_SD_CARD_ERROR: "SD-kortfel"
|
||||
STR_WIFI_NETWORKS: "Trådlösa nätverk"
|
||||
STR_NO_NETWORKS: "Inga nätverk funna"
|
||||
STR_NETWORKS_FOUND: "%zu nätverk funna"
|
||||
STR_SCANNING: "Scannar…"
|
||||
STR_CONNECTING: "Ansluter…"
|
||||
STR_CONNECTED: "Ansluten!"
|
||||
STR_CONNECTION_FAILED: "Anslutning misslyckades"
|
||||
STR_CONNECTION_TIMEOUT: "Anslutnings timeout"
|
||||
STR_FORGET_NETWORK: "Glöm nätverk?"
|
||||
STR_SAVE_PASSWORD: "Spara lösenord till nästa gång?"
|
||||
STR_REMOVE_PASSWORD: "Radera sparat lösenord?"
|
||||
STR_PRESS_OK_SCAN: "Tryck OK för att skanna igen"
|
||||
STR_PRESS_ANY_CONTINUE: "Tryck valfri knapp för att fortsätta"
|
||||
STR_SELECT_HINT: "VÄNSTER/HÖGER: Välj OK: Bekräfta"
|
||||
STR_HOW_CONNECT: "Hur vill du ansluta?"
|
||||
STR_JOIN_NETWORK: "Anslut till ett nätverk"
|
||||
STR_CREATE_HOTSPOT: "Skapa surfzon"
|
||||
STR_JOIN_DESC: "Anslut till ett befintligt trådlöst nätverk"
|
||||
STR_HOTSPOT_DESC: "Skapa ett trådlöst nätverk andra kan ansluta till"
|
||||
STR_STARTING_HOTSPOT: "Startar surfzon…"
|
||||
STR_HOTSPOT_MODE: "Surfzonsläge"
|
||||
STR_CONNECT_WIFI_HINT: "Anslut din enhet till detta trådlösa nätverk"
|
||||
STR_OPEN_URL_HINT: "Öppna denna adress i din browser"
|
||||
STR_OR_HTTP_PREFIX: "eller http://"
|
||||
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Trådlöst"
|
||||
STR_CALIBRE_WEB_URL: "Calibre webbadress"
|
||||
STR_CONNECT_WIRELESS: "Anslut som trådlös enhet"
|
||||
STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
|
||||
STR_MAC_ADDRESS: "MAC-adress:"
|
||||
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
|
||||
STR_ENTER_WIFI_PASSWORD: "Skriv in WiFi-lösenord"
|
||||
STR_ENTER_TEXT: "Skriv text"
|
||||
STR_TO_PREFIX: "till"
|
||||
STR_CALIBRE_DISCOVERING: "Söker Calibre…"
|
||||
STR_CALIBRE_CONNECTING_TO: "Ansluter till"
|
||||
STR_CALIBRE_CONNECTED_TO: "Ansluten till"
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Väntar på kommandon…"
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Anslutning misslyckades. Försöker igen)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre nedkopplat"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Väntar på överföring…"
|
||||
STR_CALIBRE_TRANSFER_HINT: "Om överföring misslyckas: Aktivera\\n'Ignorera fritt utrymme' i Calibre's\\nSmartDevice plugin settings."
|
||||
STR_CALIBRE_RECEIVING: "Tar emot:"
|
||||
STR_CALIBRE_RECEIVED: "Mottaget:"
|
||||
STR_CALIBRE_WAITING_MORE: "Väntar på mer.."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Misslyckades att skapa fil"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Lösenord krävs"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Överföring avbröts"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Installera CrossPoint Reader plugin"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Anslut till samma trådlösa nätverk"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: ”Skicka till enhet”"
|
||||
STR_CALIBRE_INSTRUCTION_4: "”Håll denna skärm öppen under sändning”"
|
||||
STR_CAT_DISPLAY: "Skärm"
|
||||
STR_CAT_READER: "Läsare"
|
||||
STR_CAT_CONTROLS: "Kontroller"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
STR_SLEEP_SCREEN: "Viloskärm"
|
||||
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
||||
STR_STATUS_BAR: "Statusrad"
|
||||
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
||||
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
||||
STR_TEXT_AA: "Textkantutjämning"
|
||||
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||
STR_ORIENTATION: "Läsrikting"
|
||||
STR_FRONT_BTN_LAYOUT: "Frontknappslayout"
|
||||
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
||||
STR_LONG_PRESS_SKIP: "Lång-tryck Kapitelskippning"
|
||||
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
||||
STR_EXT_READER_FONT: "Extern Eboksläsartypsnitt"
|
||||
STR_EXT_CHINESE_FONT: "Eboksläsartypsnitt"
|
||||
STR_EXT_UI_FONT: "Användargränssnittets typsnitt"
|
||||
STR_FONT_SIZE: "Användargränssnittets typsnittsstorlek"
|
||||
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
|
||||
STR_ASCII_LETTER_SPACING: "ASCII-bokstavsmellanrum"
|
||||
STR_ASCII_DIGIT_SPACING: "ASCII-siffermellanrum"
|
||||
STR_CJK_SPACING: "CJK-mellanrum"
|
||||
STR_COLOR_MODE: "Färgläge"
|
||||
STR_SCREEN_MARGIN: "Eboksläsarens skärmmarginal"
|
||||
STR_PARA_ALIGNMENT: "Eboksläsarens paragraflinjeplacering"
|
||||
STR_HYPHENATION: "Avstavning"
|
||||
STR_TIME_TO_SLEEP: "Tid för att gå i vila"
|
||||
STR_REFRESH_FREQ: "Uppdateringsfrekvens"
|
||||
STR_CALIBRE_SETTINGS: "Calibreinställningar"
|
||||
STR_KOREADER_SYNC: "KorReader-synkronisering"
|
||||
STR_CHECK_UPDATES: "Kolla efter uppdateringar"
|
||||
STR_LANGUAGE: "Språk"
|
||||
STR_SELECT_WALLPAPER: "Välj bakgrundsbild"
|
||||
STR_CLEAR_READING_CACHE: "Rensa Eboksläsarens cache"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Användarnamn"
|
||||
STR_PASSWORD: "Lösenord"
|
||||
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
||||
STR_AUTHENTICATE: "Autentisera "
|
||||
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
||||
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
||||
STR_FILENAME: "Filnamn"
|
||||
STR_BINARY: "Binär"
|
||||
STR_SET_CREDENTIALS_FIRST: "Referenser"
|
||||
STR_WIFI_CONN_FAILED: "Trådlös anslutning misslyckades"
|
||||
STR_AUTHENTICATING: "Autentiserar…"
|
||||
STR_AUTH_SUCCESS: "Lyckad autentisering!"
|
||||
STR_KOREADER_AUTH: "KORreader autentisering"
|
||||
STR_SYNC_READY: "KOReader synk är redo att användas"
|
||||
STR_AUTH_FAILED: "Autentisering misslyckades"
|
||||
STR_DONE: "Klar"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Detta rensar all cachad bokdata"
|
||||
STR_CLEAR_CACHE_WARNING_2: "Alla läsframsteg kommer att försvinna!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Böcker kommer att behöva omindexeras"
|
||||
STR_CLEAR_CACHE_WARNING_4: "när de öppnas på nytt."
|
||||
STR_CLEARING_CACHE: "Rensar cache…"
|
||||
STR_CACHE_CLEARED: "Cache rensad!"
|
||||
STR_ITEMS_REMOVED: "objekt raderade"
|
||||
STR_FAILED_LOWER: "misslyckades "
|
||||
STR_CLEAR_CACHE_FAILED: "Misslyckades att rensa cache"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Kolla seriell utgång för detaljer"
|
||||
STR_DARK: "Mörk"
|
||||
STR_LIGHT: "Ljus"
|
||||
STR_CUSTOM: "Valfri"
|
||||
STR_COVER: "Omslag"
|
||||
STR_NONE_OPT: "Ingen öppen bok"
|
||||
STR_FIT: "Passa"
|
||||
STR_CROP: "Beskär"
|
||||
STR_NO_PROGRESS: "Ingen framgång"
|
||||
STR_FULL_OPT: "Full"
|
||||
STR_NEVER: "Aldrig"
|
||||
STR_IN_READER: "I Eboksläsare"
|
||||
STR_ALWAYS: "Alltid"
|
||||
STR_IGNORE: "Ignorera"
|
||||
STR_SLEEP: "Vila"
|
||||
STR_PAGE_TURN: "Sidvändning"
|
||||
STR_PORTRAIT: "Porträtt"
|
||||
STR_LANDSCAPE_CW: "Landskap medurs"
|
||||
STR_INVERTED: "Inverterad"
|
||||
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||
STR_FRONT_LAYOUT_BCLR: "Bak, Bekr,Vän, Hög"
|
||||
STR_FRONT_LAYOUT_LRBC: "Vän, Hög, Bak, Bekr"
|
||||
STR_FRONT_LAYOUT_LBCR: "Vän, Bak, Bekr, Hög"
|
||||
STR_PREV_NEXT: "Förra/Nästa"
|
||||
STR_NEXT_PREV: "Nästa/Förra"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Öppen dyslektisk"
|
||||
STR_SMALL: "Liten"
|
||||
STR_MEDIUM: "Medium"
|
||||
STR_LARGE: "Stor"
|
||||
STR_X_LARGE: "Extra stor"
|
||||
STR_TIGHT: "Smal"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Bred"
|
||||
STR_JUSTIFY: "Rättfärdiga"
|
||||
STR_ALIGN_LEFT: "Vänster"
|
||||
STR_CENTER: "Mitten"
|
||||
STR_ALIGN_RIGHT: "Höger"
|
||||
STR_MIN_1: "1 min"
|
||||
STR_MIN_5: "5 min"
|
||||
STR_MIN_10: "10 min"
|
||||
STR_MIN_15: "15 min"
|
||||
STR_MIN_30: "30 min"
|
||||
STR_PAGES_1: "1 sida"
|
||||
STR_PAGES_5: "5 sidor"
|
||||
STR_PAGES_10: "10 sidor"
|
||||
STR_PAGES_15: "15 sidor"
|
||||
STR_PAGES_30: "30 sidor"
|
||||
STR_UPDATE: "Uppdatera"
|
||||
STR_CHECKING_UPDATE: "Söker uppdatering…"
|
||||
STR_NEW_UPDATE: "Ny uppdatering tillgänglig!"
|
||||
STR_CURRENT_VERSION: "Nuvarande version:"
|
||||
STR_NEW_VERSION: "Ny version:"
|
||||
STR_UPDATING: "Uppdaterar…"
|
||||
STR_NO_UPDATE: "Ingen uppdatering tillgänglig"
|
||||
STR_UPDATE_FAILED: "Uppdatering misslyckades"
|
||||
STR_UPDATE_COMPLETE: "Uppdatering färdig"
|
||||
STR_POWER_ON_HINT: "Tryck och håll strömknappen för att sätta på igen"
|
||||
STR_EXTERNAL_FONT: "Externt typsnitt"
|
||||
STR_BUILTIN_DISABLED: "Inbyggd (Avstängd)"
|
||||
STR_NO_ENTRIES: "Inga poster funna"
|
||||
STR_DOWNLOADING: "Laddar ner…"
|
||||
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
|
||||
STR_ERROR_MSG: "Fel:"
|
||||
STR_UNNAMED: "Ej namngiven"
|
||||
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
|
||||
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
|
||||
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
|
||||
STR_NETWORK_PREFIX: "Nätverk:"
|
||||
STR_IP_ADDRESS_PREFIX: "IP-adress;"
|
||||
STR_SCAN_QR_WIFI_HINT: "eller skanna QR-kod med din telefon för att ansluta till WiFi."
|
||||
STR_ERROR_GENERAL_FAILURE: "Fel: Generellt fel"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Fel: Nätverk hittades inte"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Fel: Anslutningstimeout"
|
||||
STR_SD_CARD: "SD-kort"
|
||||
STR_BACK: "« Bak"
|
||||
STR_EXIT: "« Avsluta"
|
||||
STR_HOME: "« Hem"
|
||||
STR_SAVE: "« Spara"
|
||||
STR_SELECT: "Välj "
|
||||
STR_TOGGLE: "Växla"
|
||||
STR_CONFIRM: "Bekräfta"
|
||||
STR_CANCEL: "Avbryt"
|
||||
STR_CONNECT: "Anslut"
|
||||
STR_OPEN: "Öppna"
|
||||
STR_DOWNLOAD: "Ladda ner"
|
||||
STR_RETRY: "Försök igen"
|
||||
STR_YES: "Ja"
|
||||
STR_NO: "Nej"
|
||||
STR_STATE_ON: "PÅ"
|
||||
STR_STATE_OFF: "AV"
|
||||
STR_SET: "Inställd"
|
||||
STR_NOT_SET: "Inte inställd"
|
||||
STR_DIR_LEFT: "Vänster"
|
||||
STR_DIR_RIGHT: "Höger"
|
||||
STR_DIR_UP: "Upp"
|
||||
STR_DIR_DOWN: "Ner"
|
||||
STR_CAPS_ON: "VERSALER"
|
||||
STR_CAPS_OFF: "versaler"
|
||||
STR_OK_BUTTON: "Okej"
|
||||
STR_ON_MARKER: "[PÅ]"
|
||||
STR_SLEEP_COVER_FILTER: "Viloskärmens omslagsfilter"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Procent"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Full w/ Boklist"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Boklist enbart"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist"
|
||||
STR_UI_THEME: "Användargränssnittstema"
|
||||
STR_THEME_CLASSIC: "Klassisk"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
||||
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
||||
STR_OPDS_BROWSER: "OPDS-webbläsare"
|
||||
STR_COVER_CUSTOM: "Omslag + Valfri"
|
||||
STR_RECENTS: "Senaste"
|
||||
STR_MENU_RECENT_BOOKS: "Senaste böckerna"
|
||||
STR_NO_RECENT_BOOKS: "Inga senaste böcker"
|
||||
STR_CALIBRE_DESC: "Använd Calibres trådlösa enhetsöverföring"
|
||||
STR_FORGET_AND_REMOVE: "Glöm nätverk och ta bort sparat lösenord?"
|
||||
STR_FORGET_BUTTON: "Glöm nätverk"
|
||||
STR_CALIBRE_STARTING: "Starar Calibre…"
|
||||
STR_CALIBRE_SETUP: "Inställning"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Rensa"
|
||||
STR_DEFAULT_VALUE: "Standard"
|
||||
STR_REMAP_PROMPT: "Tryck en frontknapp för var funktion"
|
||||
STR_UNASSIGNED: "Otilldelad"
|
||||
STR_ALREADY_ASSIGNED: "Redan tilldelad"
|
||||
STR_REMAP_RESET_HINT: "Översta sidoknapp: Återställ standardlayout"
|
||||
STR_REMAP_CANCEL_HINT: "Nedre sidoknapp: Avbryt tilldelning"
|
||||
STR_HW_BACK_LABEL: "Bak (Första knapp)"
|
||||
STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knapp)"
|
||||
STR_HW_LEFT_LABEL: "Vänster (Tredje knapp)"
|
||||
STR_HW_RIGHT_LABEL: "Höger (Fjärde knapp)"
|
||||
STR_GO_TO_PERCENT: "Gå till %"
|
||||
STR_GO_HOME_BUTTON: "Gå Hem"
|
||||
STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
|
||||
STR_DELETE_CACHE: "Radera bokcache"
|
||||
STR_CHAPTER_PREFIX: "Kapitel:"
|
||||
STR_PAGES_SEPARATOR: " sidor | "
|
||||
STR_BOOK_PREFIX: "Bok:"
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "LOCK"
|
||||
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
|
||||
STR_PERCENT_STEP_HINT: "Vänster/Höger: 1% Upp/Ner 10%"
|
||||
STR_SYNCING_TIME: "Synkroniserar tid…"
|
||||
STR_CALC_HASH: "Beräknar dokumenthash"
|
||||
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
|
||||
STR_FETCH_PROGRESS: "Hämtar fjärrframsteg"
|
||||
STR_UPLOAD_PROGRESS: "Laddar upp framsteg"
|
||||
STR_NO_CREDENTIALS_MSG: "Inga uppgifter inställda"
|
||||
STR_KOREADER_SETUP_HINT: "Ställ in KOReaderkonto i Inställningar"
|
||||
STR_PROGRESS_FOUND: "Framsteg funna!"
|
||||
STR_REMOTE_LABEL: "Fjärr:"
|
||||
STR_LOCAL_LABEL: "Lokalt:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Sida %d, %.2f%% totalt"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Sida %d/%d, %.2f%% totalt"
|
||||
STR_DEVICE_FROM_FORMAT: " Från: %s"
|
||||
STR_APPLY_REMOTE: "Använd fjärrframsteg"
|
||||
STR_UPLOAD_LOCAL: "Ladda upp lokala framsteg"
|
||||
STR_NO_REMOTE_MSG: "Inga fjärrframsteg funna"
|
||||
STR_UPLOAD_PROMPT: "Ladda upp nuvarande position?"
|
||||
STR_UPLOAD_SUCCESS: "Framsteg uppladdade!"
|
||||
STR_SYNC_FAILED_MSG: "Synkronisering misslyckades"
|
||||
STR_SECTION_PREFIX: "Sektion"
|
||||
STR_UPLOAD: "Uppladdning"
|
||||
STR_BOOK_S_STYLE: "Bokstil"
|
||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "JpegToBmpConverter.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <picojpeg.h>
|
||||
|
||||
#include <cstdio>
|
||||
@@ -201,8 +201,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
|
||||
// Internal implementation with configurable target size and bit depth
|
||||
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||
bool oneBit, bool crop) {
|
||||
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit",
|
||||
targetWidth, targetHeight);
|
||||
LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||
|
||||
// Setup context for picojpeg callback
|
||||
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
||||
@@ -211,12 +210,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
pjpeg_image_info_t imageInfo;
|
||||
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] JPEG decode init failed with error code: %d\n", millis(), status);
|
||||
LOG_ERR("JPG", "JPEG decode init failed with error code: %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [JPG] JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d\n", millis(), imageInfo.m_width,
|
||||
imageInfo.m_height, imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
||||
LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||
imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
||||
|
||||
// Safety limits to prevent memory issues on ESP32
|
||||
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||
@@ -224,8 +223,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
constexpr int MAX_MCU_ROW_BYTES = 65536;
|
||||
|
||||
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
|
||||
Serial.printf("[%lu] [JPG] Image too large (%dx%d), max supported: %dx%d\n", millis(), imageInfo.m_width,
|
||||
imageInfo.m_height, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
||||
LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||
MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -262,8 +261,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
|
||||
needsScaling = true;
|
||||
|
||||
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
|
||||
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight);
|
||||
LOG_DBG("JPG", "Pre-scaling %dx%d -> %dx%d (fit to %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth,
|
||||
outHeight, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
// Write BMP header with output dimensions
|
||||
@@ -282,7 +281,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
// Allocate row buffer
|
||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||
if (!rowBuffer) {
|
||||
Serial.printf("[%lu] [JPG] Failed to allocate row buffer\n", millis());
|
||||
LOG_ERR("JPG", "Failed to allocate row buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -293,15 +292,14 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
|
||||
// Validate MCU row buffer size before allocation
|
||||
if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
|
||||
Serial.printf("[%lu] [JPG] MCU row buffer too large (%d bytes), max: %d\n", millis(), mcuRowPixels,
|
||||
MAX_MCU_ROW_BYTES);
|
||||
LOG_DBG("JPG", "MCU row buffer too large (%d bytes), max: %d", mcuRowPixels, MAX_MCU_ROW_BYTES);
|
||||
free(rowBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
|
||||
if (!mcuRowBuffer) {
|
||||
Serial.printf("[%lu] [JPG] Failed to allocate MCU row buffer (%d bytes)\n", millis(), mcuRowPixels);
|
||||
LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels);
|
||||
free(rowBuffer);
|
||||
return false;
|
||||
}
|
||||
@@ -349,10 +347,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
const unsigned char mcuStatus = pjpeg_decode_mcu();
|
||||
if (mcuStatus != 0) {
|
||||
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
|
||||
Serial.printf("[%lu] [JPG] Unexpected end of blocks at MCU (%d, %d)\n", millis(), mcuX, mcuY);
|
||||
LOG_ERR("JPG", "Unexpected end of blocks at MCU (%d, %d)", mcuX, mcuY);
|
||||
} else {
|
||||
Serial.printf("[%lu] [JPG] JPEG decode MCU failed at (%d, %d) with error code: %d\n", millis(), mcuX, mcuY,
|
||||
mcuStatus);
|
||||
LOG_ERR("JPG", "JPEG decode MCU failed at (%d, %d) with error code: %d", mcuX, mcuY, mcuStatus);
|
||||
}
|
||||
free(mcuRowBuffer);
|
||||
free(rowBuffer);
|
||||
@@ -549,7 +546,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
free(mcuRowBuffer);
|
||||
free(rowBuffer);
|
||||
|
||||
Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis());
|
||||
LOG_DBG("JPG", "Successfully converted JPEG to BMP");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "KOReaderCredentialStore.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <MD5Builder.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
// Initialize the static instance
|
||||
@@ -32,10 +32,10 @@ void KOReaderCredentialStore::obfuscate(std::string& data) const {
|
||||
|
||||
bool KOReaderCredentialStore::saveToFile() const {
|
||||
// Make sure the directory exists
|
||||
SdMan.mkdir("/.crosspoint");
|
||||
Storage.mkdir("/.crosspoint");
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
||||
if (!Storage.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ bool KOReaderCredentialStore::saveToFile() const {
|
||||
|
||||
// Write username (plaintext - not particularly sensitive)
|
||||
serialization::writeString(file, username);
|
||||
Serial.printf("[%lu] [KRS] Saving username: %s\n", millis(), username.c_str());
|
||||
LOG_DBG("KRS", "Saving username: %s", username.c_str());
|
||||
|
||||
// Write password (obfuscated)
|
||||
std::string obfuscatedPwd = password;
|
||||
@@ -58,14 +58,14 @@ bool KOReaderCredentialStore::saveToFile() const {
|
||||
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
||||
|
||||
file.close();
|
||||
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
|
||||
LOG_DBG("KRS", "Saved KOReader credentials to file");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KOReaderCredentialStore::loadFromFile() {
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) {
|
||||
Serial.printf("[%lu] [KRS] No credentials file found\n", millis());
|
||||
if (!Storage.openFileForRead("KRS", KOREADER_FILE, file)) {
|
||||
LOG_DBG("KRS", "No credentials file found");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ bool KOReaderCredentialStore::loadFromFile() {
|
||||
uint8_t version;
|
||||
serialization::readPod(file, version);
|
||||
if (version != KOREADER_FILE_VERSION) {
|
||||
Serial.printf("[%lu] [KRS] Unknown file version: %u\n", millis(), version);
|
||||
LOG_DBG("KRS", "Unknown file version: %u", version);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@@ -110,14 +110,14 @@ bool KOReaderCredentialStore::loadFromFile() {
|
||||
}
|
||||
|
||||
file.close();
|
||||
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
|
||||
LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", username.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
||||
username = user;
|
||||
password = pass;
|
||||
Serial.printf("[%lu] [KRS] Set credentials for user: %s\n", millis(), user.c_str());
|
||||
LOG_DBG("KRS", "Set credentials for user: %s", user.c_str());
|
||||
}
|
||||
|
||||
std::string KOReaderCredentialStore::getMd5Password() const {
|
||||
@@ -140,12 +140,12 @@ void KOReaderCredentialStore::clearCredentials() {
|
||||
username.clear();
|
||||
password.clear();
|
||||
saveToFile();
|
||||
Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis());
|
||||
LOG_DBG("KRS", "Cleared KOReader credentials");
|
||||
}
|
||||
|
||||
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
||||
serverUrl = url;
|
||||
Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str());
|
||||
LOG_DBG("KRS", "Set server URL: %s", url.empty() ? "(default)" : url.c_str());
|
||||
}
|
||||
|
||||
std::string KOReaderCredentialStore::getBaseUrl() const {
|
||||
@@ -163,6 +163,5 @@ std::string KOReaderCredentialStore::getBaseUrl() const {
|
||||
|
||||
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
||||
matchMethod = method;
|
||||
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(),
|
||||
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "KOReaderDocumentId.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <MD5Builder.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
namespace {
|
||||
// Extract filename from path (everything after last '/')
|
||||
@@ -27,7 +27,7 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat
|
||||
md5.calculate();
|
||||
|
||||
std::string result = md5.toString().c_str();
|
||||
Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str());
|
||||
LOG_DBG("KODoc", "Filename hash: %s (from '%s')", result.c_str(), filename.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@ size_t KOReaderDocumentId::getOffset(int i) {
|
||||
|
||||
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("KODoc", filePath, file)) {
|
||||
Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str());
|
||||
if (!Storage.openFileForRead("KODoc", filePath, file)) {
|
||||
LOG_DBG("KODoc", "Failed to open file: %s", filePath.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
const size_t fileSize = file.fileSize();
|
||||
Serial.printf("[%lu] [KODoc] Calculating hash for file: %s (size: %zu)\n", millis(), filePath.c_str(), fileSize);
|
||||
LOG_DBG("KODoc", "Calculating hash for file: %s (size: %zu)", filePath.c_str(), fileSize);
|
||||
|
||||
// Initialize MD5 builder
|
||||
MD5Builder md5;
|
||||
@@ -70,7 +70,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
||||
|
||||
// Seek to offset
|
||||
if (!file.seekSet(offset)) {
|
||||
Serial.printf("[%lu] [KODoc] Failed to seek to offset %zu\n", millis(), offset);
|
||||
LOG_DBG("KODoc", "Failed to seek to offset %zu", offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
||||
md5.calculate();
|
||||
std::string result = md5.toString().c_str();
|
||||
|
||||
Serial.printf("[%lu] [KODoc] Hash calculated: %s (from %zu bytes)\n", millis(), result.c_str(), totalBytesRead);
|
||||
LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
@@ -30,12 +30,12 @@ bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0;
|
||||
|
||||
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||
if (!KOREADER_STORE.hasCredentials()) {
|
||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
||||
LOG_DBG("KOSync", "No credentials configured");
|
||||
return NO_CREDENTIALS;
|
||||
}
|
||||
|
||||
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
||||
Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str());
|
||||
LOG_DBG("KOSync", "Authenticating: %s", url.c_str());
|
||||
|
||||
HTTPClient http;
|
||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||
@@ -53,7 +53,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||
const int httpCode = http.GET();
|
||||
http.end();
|
||||
|
||||
Serial.printf("[%lu] [KOSync] Auth response: %d\n", millis(), httpCode);
|
||||
LOG_DBG("KOSync", "Auth response: %d", httpCode);
|
||||
|
||||
if (httpCode == 200) {
|
||||
return OK;
|
||||
@@ -68,12 +68,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
|
||||
KOReaderProgress& outProgress) {
|
||||
if (!KOREADER_STORE.hasCredentials()) {
|
||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
||||
LOG_DBG("KOSync", "No credentials configured");
|
||||
return NO_CREDENTIALS;
|
||||
}
|
||||
|
||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
||||
Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str());
|
||||
LOG_DBG("KOSync", "Getting progress: %s", url.c_str());
|
||||
|
||||
HTTPClient http;
|
||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||
@@ -99,7 +99,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
||||
const DeserializationError error = deserializeJson(doc, responseBody);
|
||||
|
||||
if (error) {
|
||||
Serial.printf("[%lu] [KOSync] JSON parse failed: %s\n", millis(), error.c_str());
|
||||
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
||||
return JSON_ERROR;
|
||||
}
|
||||
|
||||
@@ -110,14 +110,13 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
||||
outProgress.deviceId = doc["device_id"].as<std::string>();
|
||||
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
||||
|
||||
Serial.printf("[%lu] [KOSync] Got progress: %.2f%% at %s\n", millis(), outProgress.percentage * 100,
|
||||
outProgress.progress.c_str());
|
||||
LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
|
||||
return OK;
|
||||
}
|
||||
|
||||
http.end();
|
||||
|
||||
Serial.printf("[%lu] [KOSync] Get progress response: %d\n", millis(), httpCode);
|
||||
LOG_DBG("KOSync", "Get progress response: %d", httpCode);
|
||||
|
||||
if (httpCode == 401) {
|
||||
return AUTH_FAILED;
|
||||
@@ -131,12 +130,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
||||
|
||||
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
|
||||
if (!KOREADER_STORE.hasCredentials()) {
|
||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
||||
LOG_DBG("KOSync", "No credentials configured");
|
||||
return NO_CREDENTIALS;
|
||||
}
|
||||
|
||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
||||
Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str());
|
||||
LOG_DBG("KOSync", "Updating progress: %s", url.c_str());
|
||||
|
||||
HTTPClient http;
|
||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||
@@ -163,12 +162,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
||||
std::string body;
|
||||
serializeJson(doc, body);
|
||||
|
||||
Serial.printf("[%lu] [KOSync] Request body: %s\n", millis(), body.c_str());
|
||||
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
||||
|
||||
const int httpCode = http.PUT(body.c_str());
|
||||
http.end();
|
||||
|
||||
Serial.printf("[%lu] [KOSync] Update progress response: %d\n", millis(), httpCode);
|
||||
LOG_DBG("KOSync", "Update progress response: %d", httpCode);
|
||||
|
||||
if (httpCode == 200 || httpCode == 202) {
|
||||
return OK;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "ProgressMapper.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
@@ -23,8 +23,8 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
|
||||
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
|
||||
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
|
||||
|
||||
Serial.printf("[%lu] [ProgressMapper] CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s\n", millis(),
|
||||
chapterName.c_str(), pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
||||
LOG_DBG("ProgressMapper", "CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s", chapterName.c_str(),
|
||||
pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -76,8 +76,8 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [ProgressMapper] KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d\n", millis(),
|
||||
koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
||||
LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d", koPos.percentage * 100,
|
||||
koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
47
lib/Logging/Logging.cpp
Normal file
47
lib/Logging/Logging.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "Logging.h"
|
||||
|
||||
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
|
||||
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
|
||||
// provide the format string for the message itself.
|
||||
void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
||||
if (!logSerial) {
|
||||
return; // Serial not initialized, skip logging
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
char buf[256];
|
||||
char* c = buf;
|
||||
// add the timestamp
|
||||
{
|
||||
unsigned long ms = millis();
|
||||
int len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||
if (len < 0) {
|
||||
return; // encoding error, skip logging
|
||||
}
|
||||
c += len;
|
||||
}
|
||||
// add the level
|
||||
{
|
||||
const char* p = level;
|
||||
size_t remaining = sizeof(buf) - (c - buf);
|
||||
while (*p && remaining > 1) {
|
||||
*c++ = *p++;
|
||||
remaining--;
|
||||
}
|
||||
if (remaining > 1) {
|
||||
*c++ = ' ';
|
||||
}
|
||||
}
|
||||
// add the origin
|
||||
{
|
||||
int len = snprintf(c, sizeof(buf) - (c - buf), "[%s] ", origin);
|
||||
if (len < 0) {
|
||||
return; // encoding error, skip logging
|
||||
}
|
||||
c += len;
|
||||
}
|
||||
// add the user message
|
||||
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
|
||||
va_end(args);
|
||||
logSerial.print(buf);
|
||||
}
|
||||
71
lib/Logging/Logging.h
Normal file
71
lib/Logging/Logging.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
/*
|
||||
Define ENABLE_SERIAL_LOG to enable logging
|
||||
Can be set in platformio.ini build_flags or as a compile definition
|
||||
|
||||
Define LOG_LEVEL to control log verbosity:
|
||||
0 = ERR only
|
||||
1 = ERR + INF
|
||||
2 = ERR + INF + DBG
|
||||
If not defined, defaults to 0
|
||||
|
||||
If you have a legitimate need for raw Serial access (e.g., binary data,
|
||||
special formatting), use the underlying logSerial object directly:
|
||||
logSerial.printf("Special case: %d\n", value);
|
||||
logSerial.write(binaryData, length);
|
||||
|
||||
The logSerial reference (defined below) points to the real Serial object and
|
||||
won't trigger deprecation warnings.
|
||||
*/
|
||||
|
||||
#ifndef LOG_LEVEL
|
||||
#define LOG_LEVEL 0
|
||||
#endif
|
||||
|
||||
static HWCDC& logSerial = Serial;
|
||||
|
||||
void logPrintf(const char* level, const char* origin, const char* format, ...);
|
||||
|
||||
#ifdef ENABLE_SERIAL_LOG
|
||||
#if LOG_LEVEL >= 0
|
||||
#define LOG_ERR(origin, format, ...) logPrintf("[ERR]", origin, format "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_ERR(origin, format, ...)
|
||||
#endif
|
||||
|
||||
#if LOG_LEVEL >= 1
|
||||
#define LOG_INF(origin, format, ...) logPrintf("[INF]", origin, format "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_INF(origin, format, ...)
|
||||
#endif
|
||||
|
||||
#if LOG_LEVEL >= 2
|
||||
#define LOG_DBG(origin, format, ...) logPrintf("[DBG]", origin, format "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_DBG(origin, format, ...)
|
||||
#endif
|
||||
#else
|
||||
#define LOG_DBG(origin, format, ...)
|
||||
#define LOG_ERR(origin, format, ...)
|
||||
#define LOG_INF(origin, format, ...)
|
||||
#endif
|
||||
|
||||
class MySerialImpl : public Print {
|
||||
public:
|
||||
void begin(unsigned long baud) { logSerial.begin(baud); }
|
||||
|
||||
// Support boolean conversion for compatibility with code like:
|
||||
// if (Serial) or while (!Serial)
|
||||
operator bool() const { return logSerial; }
|
||||
|
||||
__attribute__((deprecated("Use LOG_* macro instead"))) size_t printf(const char* format, ...);
|
||||
size_t write(uint8_t b) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
void flush() override;
|
||||
static MySerialImpl instance;
|
||||
};
|
||||
|
||||
#define Serial MySerialImpl::instance
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "OpdsParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -8,7 +8,7 @@ OpdsParser::OpdsParser() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
errorOccured = true;
|
||||
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis());
|
||||
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
||||
void* const buf = XML_GetBuffer(parser, chunkSize);
|
||||
if (!buf) {
|
||||
errorOccured = true;
|
||||
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis());
|
||||
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
return length;
|
||||
@@ -53,8 +53,8 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
||||
errorOccured = true;
|
||||
Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
return length;
|
||||
|
||||
27
lib/PlaceholderCover/BookIcon.h
Normal file
27
lib/PlaceholderCover/BookIcon.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Book icon: 48x48, 1-bit packed (MSB first)
|
||||
// 0 = black, 1 = white (same format as Logo120.h)
|
||||
static constexpr int BOOK_ICON_WIDTH = 48;
|
||||
static constexpr int BOOK_ICON_HEIGHT = 48;
|
||||
static const uint8_t BookIcon[] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00,
|
||||
0x00, 0x1f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1c, 0x00, 0x00, 0x01, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1c, 0x00, 0x01,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1c, 0x00, 0x00, 0x1f, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f,
|
||||
0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00,
|
||||
0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
};
|
||||
480
lib/PlaceholderCover/PlaceholderCoverGenerator.cpp
Normal file
480
lib/PlaceholderCover/PlaceholderCoverGenerator.cpp
Normal file
@@ -0,0 +1,480 @@
|
||||
#include "PlaceholderCoverGenerator.h"
|
||||
|
||||
#include <EpdFont.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
// Include the UI fonts directly for self-contained placeholder rendering.
|
||||
// These are 1-bit bitmap fonts compiled from Ubuntu TTF.
|
||||
#include "builtinFonts/ubuntu_10_regular.h"
|
||||
#include "builtinFonts/ubuntu_12_bold.h"
|
||||
|
||||
// Book icon bitmap (48x48 1-bit, generated by scripts/generate_book_icon.py)
|
||||
#include "BookIcon.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// BMP writing helpers (same format as JpegToBmpConverter)
|
||||
inline void write16(Print& out, const uint16_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32(Print& out, const uint32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32Signed(Print& out, const int32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
|
||||
const int bytesPerRow = (width + 31) / 32 * 4;
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t fileSize = 62 + imageSize;
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize);
|
||||
write32(bmpOut, 0); // Reserved
|
||||
write32(bmpOut, 62); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height); // Negative = top-down
|
||||
write16(bmpOut, 1); // Color planes
|
||||
write16(bmpOut, 1); // Bits per pixel
|
||||
write32(bmpOut, 0); // BI_RGB
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835); // xPixelsPerMeter
|
||||
write32(bmpOut, 2835); // yPixelsPerMeter
|
||||
write32(bmpOut, 2); // colorsUsed
|
||||
write32(bmpOut, 2); // colorsImportant
|
||||
|
||||
// Palette: index 0 = black, index 1 = white
|
||||
const uint8_t palette[8] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Black
|
||||
0xFF, 0xFF, 0xFF, 0x00 // White
|
||||
};
|
||||
for (const uint8_t b : palette) {
|
||||
bmpOut.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// 1-bit pixel buffer that can render text, icons, and shapes, then write as BMP.
|
||||
class PixelBuffer {
|
||||
public:
|
||||
PixelBuffer(int width, int height) : width(width), height(height) {
|
||||
bytesPerRow = (width + 31) / 32 * 4;
|
||||
bufferSize = bytesPerRow * height;
|
||||
buffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
if (buffer) {
|
||||
memset(buffer, 0xFF, bufferSize); // White background
|
||||
}
|
||||
}
|
||||
|
||||
~PixelBuffer() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
bool isValid() const { return buffer != nullptr; }
|
||||
|
||||
/// Set a pixel to black.
|
||||
void setBlack(int x, int y) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
const int byteIndex = y * bytesPerRow + x / 8;
|
||||
const uint8_t bitMask = 0x80 >> (x % 8);
|
||||
buffer[byteIndex] &= ~bitMask;
|
||||
}
|
||||
|
||||
/// Set a scaled "pixel" (scale x scale block) to black.
|
||||
void setBlackScaled(int x, int y, int scale) {
|
||||
for (int dy = 0; dy < scale; dy++) {
|
||||
for (int dx = 0; dx < scale; dx++) {
|
||||
setBlack(x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a filled rectangle in black.
|
||||
void fillRect(int x, int y, int w, int h) {
|
||||
for (int row = y; row < y + h && row < height; row++) {
|
||||
for (int col = x; col < x + w && col < width; col++) {
|
||||
setBlack(col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a rectangular border in black.
|
||||
void drawBorder(int x, int y, int w, int h, int thickness) {
|
||||
fillRect(x, y, w, thickness); // Top
|
||||
fillRect(x, y + h - thickness, w, thickness); // Bottom
|
||||
fillRect(x, y, thickness, h); // Left
|
||||
fillRect(x + w - thickness, y, thickness, h); // Right
|
||||
}
|
||||
|
||||
/// Draw a horizontal line in black with configurable thickness.
|
||||
void drawHLine(int x, int y, int length, int thickness = 1) {
|
||||
fillRect(x, y, length, thickness);
|
||||
}
|
||||
|
||||
/// Render a single glyph at (cursorX, baselineY) with integer scaling. Returns advance in X (scaled).
|
||||
int renderGlyph(const EpdFontData* font, uint32_t codepoint, int cursorX, int baselineY, int scale = 1) {
|
||||
const EpdFont fontObj(font);
|
||||
const EpdGlyph* glyph = fontObj.getGlyph(codepoint);
|
||||
if (!glyph) {
|
||||
glyph = fontObj.getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
if (!glyph) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t* bitmap = &font->bitmap[glyph->dataOffset];
|
||||
const int glyphW = glyph->width;
|
||||
const int glyphH = glyph->height;
|
||||
|
||||
for (int gy = 0; gy < glyphH; gy++) {
|
||||
const int screenY = baselineY - glyph->top * scale + gy * scale;
|
||||
for (int gx = 0; gx < glyphW; gx++) {
|
||||
const int pixelPos = gy * glyphW + gx;
|
||||
const int screenX = cursorX + glyph->left * scale + gx * scale;
|
||||
|
||||
bool isSet = false;
|
||||
if (font->is2Bit) {
|
||||
const uint8_t byte = bitmap[pixelPos / 4];
|
||||
const uint8_t bitIndex = (3 - pixelPos % 4) * 2;
|
||||
const uint8_t val = 3 - ((byte >> bitIndex) & 0x3);
|
||||
isSet = (val < 3);
|
||||
} else {
|
||||
const uint8_t byte = bitmap[pixelPos / 8];
|
||||
const uint8_t bitIndex = 7 - (pixelPos % 8);
|
||||
isSet = ((byte >> bitIndex) & 1);
|
||||
}
|
||||
|
||||
if (isSet) {
|
||||
setBlackScaled(screenX, screenY, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return glyph->advanceX * scale;
|
||||
}
|
||||
|
||||
/// Render a UTF-8 string at (x, y) where y is the top of the text line, with integer scaling.
|
||||
void drawText(const EpdFontData* font, int x, int y, const char* text, int scale = 1) {
|
||||
const int baselineY = y + font->ascender * scale;
|
||||
int cursorX = x;
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
cursorX += renderGlyph(font, cp, cursorX, baselineY, scale);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a 1-bit icon bitmap (MSB first, 0=black, 1=white) with integer scaling.
|
||||
void drawIcon(const uint8_t* icon, int iconW, int iconH, int x, int y, int scale = 1) {
|
||||
const int bytesPerIconRow = iconW / 8;
|
||||
for (int iy = 0; iy < iconH; iy++) {
|
||||
for (int ix = 0; ix < iconW; ix++) {
|
||||
const int byteIdx = iy * bytesPerIconRow + ix / 8;
|
||||
const uint8_t bitMask = 0x80 >> (ix % 8);
|
||||
// In the icon data: 0 = black (drawn), 1 = white (skip)
|
||||
if (!(icon[byteIdx] & bitMask)) {
|
||||
const int sx = x + ix * scale;
|
||||
const int sy = y + iy * scale;
|
||||
setBlackScaled(sx, sy, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the pixel buffer to a file as a 1-bit BMP.
|
||||
bool writeBmp(Print& out) const {
|
||||
if (!buffer) return false;
|
||||
writeBmpHeader1bit(out, width, height);
|
||||
out.write(buffer, bufferSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
|
||||
private:
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
size_t bufferSize;
|
||||
uint8_t* buffer;
|
||||
};
|
||||
|
||||
/// Measure the width of a UTF-8 string in pixels (at 1x scale).
|
||||
int measureTextWidth(const EpdFontData* font, const char* text) {
|
||||
const EpdFont fontObj(font);
|
||||
int w = 0, h = 0;
|
||||
fontObj.getTextDimensions(text, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
/// Get the advance width of a single character.
|
||||
int getCharAdvance(const EpdFontData* font, uint32_t cp) {
|
||||
const EpdFont fontObj(font);
|
||||
const EpdGlyph* glyph = fontObj.getGlyph(cp);
|
||||
if (!glyph) return 0;
|
||||
return glyph->advanceX;
|
||||
}
|
||||
|
||||
/// Split a string into words (splitting on spaces).
|
||||
std::vector<std::string> splitWords(const std::string& text) {
|
||||
std::vector<std::string> words;
|
||||
std::string current;
|
||||
for (size_t i = 0; i < text.size(); i++) {
|
||||
if (text[i] == ' ') {
|
||||
if (!current.empty()) {
|
||||
words.push_back(current);
|
||||
current.clear();
|
||||
}
|
||||
} else {
|
||||
current += text[i];
|
||||
}
|
||||
}
|
||||
if (!current.empty()) {
|
||||
words.push_back(current);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
/// Word-wrap text into lines that fit within maxWidth pixels at the given scale.
|
||||
std::vector<std::string> wrapText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
|
||||
std::vector<std::string> lines;
|
||||
const auto words = splitWords(text);
|
||||
if (words.empty()) return lines;
|
||||
|
||||
const int spaceWidth = getCharAdvance(font, ' ') * scale;
|
||||
std::string currentLine;
|
||||
int currentWidth = 0;
|
||||
|
||||
for (const auto& word : words) {
|
||||
const int wordWidth = measureTextWidth(font, word.c_str()) * scale;
|
||||
|
||||
if (currentLine.empty()) {
|
||||
currentLine = word;
|
||||
currentWidth = wordWidth;
|
||||
} else if (currentWidth + spaceWidth + wordWidth <= maxWidth) {
|
||||
currentLine += " " + word;
|
||||
currentWidth += spaceWidth + wordWidth;
|
||||
} else {
|
||||
lines.push_back(currentLine);
|
||||
currentLine = word;
|
||||
currentWidth = wordWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentLine.empty()) {
|
||||
lines.push_back(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// Truncate a string with "..." if it exceeds maxWidth pixels at the given scale.
|
||||
std::string truncateText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
|
||||
if (measureTextWidth(font, text.c_str()) * scale <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string truncated = text;
|
||||
const char* ellipsis = "...";
|
||||
const int ellipsisWidth = measureTextWidth(font, ellipsis) * scale;
|
||||
|
||||
while (!truncated.empty()) {
|
||||
utf8RemoveLastChar(truncated);
|
||||
if (measureTextWidth(font, truncated.c_str()) * scale + ellipsisWidth <= maxWidth) {
|
||||
return truncated + ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
return ellipsis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PlaceholderCoverGenerator::generate(const std::string& outputPath, const std::string& title,
|
||||
const std::string& author, int width, int height) {
|
||||
LOG_DBG("PHC", "Generating placeholder cover %dx%d: \"%s\" by \"%s\"", width, height, title.c_str(), author.c_str());
|
||||
|
||||
const EpdFontData* titleFont = &ubuntu_12_bold;
|
||||
const EpdFontData* authorFont = &ubuntu_10_regular;
|
||||
|
||||
PixelBuffer buf(width, height);
|
||||
if (!buf.isValid()) {
|
||||
LOG_ERR("PHC", "Failed to allocate %dx%d pixel buffer (%d bytes)", width, height,
|
||||
(width + 31) / 32 * 4 * height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Proportional layout constants based on cover dimensions.
|
||||
// The device bezel covers ~2-3px on each edge, so we pad inward from the edge.
|
||||
const int edgePadding = std::max(3, width / 48); // ~10px at 480w, ~3px at 136w
|
||||
const int borderWidth = std::max(2, width / 96); // ~5px at 480w, ~2px at 136w
|
||||
const int innerPadding = std::max(4, width / 32); // ~15px at 480w, ~4px at 136w
|
||||
|
||||
// Text scaling: 2x for full-size covers, 1x for thumbnails
|
||||
const int titleScale = (height >= 600) ? 2 : 1;
|
||||
const int authorScale = (height >= 600) ? 2 : 1; // Author also larger on full covers
|
||||
// Icon: 2x for full cover, 1x for medium thumb, skip for small
|
||||
const int iconScale = (height >= 600) ? 2 : (height >= 350 ? 1 : 0);
|
||||
|
||||
// Draw border inset from edge
|
||||
buf.drawBorder(edgePadding, edgePadding, width - 2 * edgePadding, height - 2 * edgePadding, borderWidth);
|
||||
|
||||
// Content area (inside border + inner padding)
|
||||
const int contentX = edgePadding + borderWidth + innerPadding;
|
||||
const int contentY = edgePadding + borderWidth + innerPadding;
|
||||
const int contentW = width - 2 * contentX;
|
||||
const int contentH = height - 2 * contentY;
|
||||
|
||||
if (contentW <= 0 || contentH <= 0) {
|
||||
LOG_ERR("PHC", "Cover too small for content (%dx%d)", width, height);
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
|
||||
return false;
|
||||
}
|
||||
buf.writeBmp(file);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Layout zones ---
|
||||
// Title zone: top 2/3 of content area (icon + title)
|
||||
// Author zone: bottom 1/3 of content area
|
||||
const int titleZoneH = contentH * 2 / 3;
|
||||
const int authorZoneH = contentH - titleZoneH;
|
||||
const int authorZoneY = contentY + titleZoneH;
|
||||
|
||||
// --- Separator line at the zone boundary ---
|
||||
const int separatorWidth = contentW / 3;
|
||||
const int separatorX = contentX + (contentW - separatorWidth) / 2;
|
||||
buf.drawHLine(separatorX, authorZoneY, separatorWidth);
|
||||
|
||||
// --- Icon dimensions (needed for title text wrapping) ---
|
||||
const int iconW = (iconScale > 0) ? BOOK_ICON_WIDTH * iconScale : 0;
|
||||
const int iconGap = (iconScale > 0) ? std::max(8, width / 40) : 0; // Gap between icon and title text
|
||||
const int titleTextW = contentW - iconW - iconGap; // Title wraps in narrower area beside icon
|
||||
|
||||
// --- Prepare title text (wraps within the area to the right of the icon) ---
|
||||
const std::string displayTitle = title.empty() ? "Untitled" : title;
|
||||
auto titleLines = wrapText(titleFont, displayTitle, titleTextW, titleScale);
|
||||
|
||||
constexpr int MAX_TITLE_LINES = 5;
|
||||
if (static_cast<int>(titleLines.size()) > MAX_TITLE_LINES) {
|
||||
titleLines.resize(MAX_TITLE_LINES);
|
||||
titleLines.back() = truncateText(titleFont, titleLines.back(), titleTextW, titleScale);
|
||||
}
|
||||
|
||||
// --- Prepare author text (multi-line, max 3 lines) ---
|
||||
std::vector<std::string> authorLines;
|
||||
if (!author.empty()) {
|
||||
authorLines = wrapText(authorFont, author, contentW, authorScale);
|
||||
constexpr int MAX_AUTHOR_LINES = 3;
|
||||
if (static_cast<int>(authorLines.size()) > MAX_AUTHOR_LINES) {
|
||||
authorLines.resize(MAX_AUTHOR_LINES);
|
||||
authorLines.back() = truncateText(authorFont, authorLines.back(), contentW, authorScale);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Calculate title zone layout (icon LEFT of title) ---
|
||||
// Tighter line spacing so 2-3 title lines fit within the icon height
|
||||
const int titleLineH = titleFont->advanceY * titleScale * 3 / 4;
|
||||
const int iconH = (iconScale > 0) ? BOOK_ICON_HEIGHT * iconScale : 0;
|
||||
const int numTitleLines = static_cast<int>(titleLines.size());
|
||||
// Visual height: distance from top of first line to bottom of last line's glyphs.
|
||||
// Use ascender (not full advanceY) for the last line since trailing line-gap isn't visible.
|
||||
const int titleVisualH = (numTitleLines > 0)
|
||||
? (numTitleLines - 1) * titleLineH + titleFont->ascender * titleScale
|
||||
: 0;
|
||||
const int titleBlockH = std::max(iconH, titleVisualH); // Taller of icon or text
|
||||
|
||||
int titleStartY = contentY + (titleZoneH - titleBlockH) / 2;
|
||||
if (titleStartY < contentY) {
|
||||
titleStartY = contentY;
|
||||
}
|
||||
|
||||
// If title fits within icon height, center it vertically against the icon.
|
||||
// Otherwise top-align so extra lines overflow below.
|
||||
const int iconY = titleStartY;
|
||||
const int titleTextY = (iconH > 0 && titleVisualH <= iconH)
|
||||
? titleStartY + (iconH - titleVisualH) / 2
|
||||
: titleStartY;
|
||||
|
||||
// --- Horizontal centering: measure the widest title line, then center icon+gap+text block ---
|
||||
int maxTitleLineW = 0;
|
||||
for (const auto& line : titleLines) {
|
||||
const int w = measureTextWidth(titleFont, line.c_str()) * titleScale;
|
||||
if (w > maxTitleLineW) maxTitleLineW = w;
|
||||
}
|
||||
const int titleBlockW = iconW + iconGap + maxTitleLineW;
|
||||
const int titleBlockX = contentX + (contentW - titleBlockW) / 2;
|
||||
|
||||
// --- Draw icon ---
|
||||
if (iconScale > 0) {
|
||||
buf.drawIcon(BookIcon, BOOK_ICON_WIDTH, BOOK_ICON_HEIGHT, titleBlockX, iconY, iconScale);
|
||||
}
|
||||
|
||||
// --- Draw title lines (to the right of the icon) ---
|
||||
const int titleTextX = titleBlockX + iconW + iconGap;
|
||||
int currentY = titleTextY;
|
||||
for (const auto& line : titleLines) {
|
||||
buf.drawText(titleFont, titleTextX, currentY, line.c_str(), titleScale);
|
||||
currentY += titleLineH;
|
||||
}
|
||||
|
||||
// --- Draw author lines (centered vertically in bottom 1/3, centered horizontally) ---
|
||||
if (!authorLines.empty()) {
|
||||
const int authorLineH = authorFont->advanceY * authorScale;
|
||||
const int authorBlockH = static_cast<int>(authorLines.size()) * authorLineH;
|
||||
int authorStartY = authorZoneY + (authorZoneH - authorBlockH) / 2;
|
||||
if (authorStartY < authorZoneY + 4) {
|
||||
authorStartY = authorZoneY + 4; // Small gap below separator
|
||||
}
|
||||
|
||||
for (const auto& line : authorLines) {
|
||||
const int lineWidth = measureTextWidth(authorFont, line.c_str()) * authorScale;
|
||||
const int lineX = contentX + (contentW - lineWidth) / 2;
|
||||
buf.drawText(authorFont, lineX, authorStartY, line.c_str(), authorScale);
|
||||
authorStartY += authorLineH;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Write to file ---
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
|
||||
LOG_ERR("PHC", "Failed to open output file: %s", outputPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool success = buf.writeBmp(file);
|
||||
file.close();
|
||||
|
||||
if (success) {
|
||||
LOG_DBG("PHC", "Placeholder cover written to %s", outputPath.c_str());
|
||||
} else {
|
||||
LOG_ERR("PHC", "Failed to write placeholder BMP");
|
||||
Storage.remove(outputPath.c_str());
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
14
lib/PlaceholderCover/PlaceholderCoverGenerator.h
Normal file
14
lib/PlaceholderCover/PlaceholderCoverGenerator.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/// Generates simple 1-bit BMP placeholder covers with title/author text
|
||||
/// for books that have no embedded cover image.
|
||||
class PlaceholderCoverGenerator {
|
||||
public:
|
||||
/// Generate a placeholder cover BMP with title and author text.
|
||||
/// The BMP is written to outputPath as a 1-bit black-and-white image.
|
||||
/// Returns true if the file was written successfully.
|
||||
static bool generate(const std::string& outputPath, const std::string& title, const std::string& author, int width,
|
||||
int height);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
|
||||
Txt::Txt(std::string path, std::string cacheBasePath)
|
||||
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
|
||||
@@ -15,14 +16,14 @@ bool Txt::load() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SdMan.exists(filepath.c_str())) {
|
||||
Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str());
|
||||
if (!Storage.exists(filepath.c_str())) {
|
||||
LOG_ERR("TXT", "File does not exist: %s", filepath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
||||
Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str());
|
||||
if (!Storage.openFileForRead("TXT", filepath, file)) {
|
||||
LOG_ERR("TXT", "Failed to open file: %s", filepath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -30,7 +31,7 @@ bool Txt::load() {
|
||||
file.close();
|
||||
|
||||
loaded = true;
|
||||
Serial.printf("[%lu] [TXT] Loaded TXT file: %s (%zu bytes)\n", millis(), filepath.c_str(), fileSize);
|
||||
LOG_DBG("TXT", "Loaded TXT file: %s (%zu bytes)", filepath.c_str(), fileSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -48,11 +49,11 @@ std::string Txt::getTitle() const {
|
||||
}
|
||||
|
||||
void Txt::setupCacheDir() const {
|
||||
if (!SdMan.exists(cacheBasePath.c_str())) {
|
||||
SdMan.mkdir(cacheBasePath.c_str());
|
||||
if (!Storage.exists(cacheBasePath.c_str())) {
|
||||
Storage.mkdir(cacheBasePath.c_str());
|
||||
}
|
||||
if (!SdMan.exists(cachePath.c_str())) {
|
||||
SdMan.mkdir(cachePath.c_str());
|
||||
if (!Storage.exists(cachePath.c_str())) {
|
||||
Storage.mkdir(cachePath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +74,8 @@ std::string Txt::findCoverImage() const {
|
||||
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
|
||||
for (const auto& ext : extensions) {
|
||||
std::string coverPath = folder + "/" + baseName + ext;
|
||||
if (SdMan.exists(coverPath.c_str())) {
|
||||
Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str());
|
||||
if (Storage.exists(coverPath.c_str())) {
|
||||
LOG_DBG("TXT", "Found matching cover image: %s", coverPath.c_str());
|
||||
return coverPath;
|
||||
}
|
||||
}
|
||||
@@ -84,8 +85,8 @@ std::string Txt::findCoverImage() const {
|
||||
for (const auto& name : coverNames) {
|
||||
for (const auto& ext : extensions) {
|
||||
std::string coverPath = folder + "/" + std::string(name) + ext;
|
||||
if (SdMan.exists(coverPath.c_str())) {
|
||||
Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str());
|
||||
if (Storage.exists(coverPath.c_str())) {
|
||||
LOG_DBG("TXT", "Found fallback cover image: %s", coverPath.c_str());
|
||||
return coverPath;
|
||||
}
|
||||
}
|
||||
@@ -96,15 +97,18 @@ std::string Txt::findCoverImage() const {
|
||||
|
||||
std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||
|
||||
std::string Txt::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||
std::string Txt::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||
|
||||
bool Txt::generateCoverBmp() const {
|
||||
// Already generated, return true
|
||||
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
||||
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string coverImagePath = findCoverImage();
|
||||
if (coverImagePath.empty()) {
|
||||
Serial.printf("[%lu] [TXT] No cover image found for TXT file\n", millis());
|
||||
LOG_DBG("TXT", "No cover image found for TXT file");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -120,12 +124,12 @@ bool Txt::generateCoverBmp() const {
|
||||
|
||||
if (isBmp) {
|
||||
// Copy BMP file to cache
|
||||
Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis());
|
||||
LOG_DBG("TXT", "Copying BMP cover image to cache");
|
||||
FsFile src, dst;
|
||||
if (!SdMan.openFileForRead("TXT", coverImagePath, src)) {
|
||||
if (!Storage.openFileForRead("TXT", coverImagePath, src)) {
|
||||
return false;
|
||||
}
|
||||
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
||||
if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
||||
src.close();
|
||||
return false;
|
||||
}
|
||||
@@ -136,18 +140,18 @@ bool Txt::generateCoverBmp() const {
|
||||
}
|
||||
src.close();
|
||||
dst.close();
|
||||
Serial.printf("[%lu] [TXT] Copied BMP cover to cache\n", millis());
|
||||
LOG_DBG("TXT", "Copied BMP cover to cache");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isJpg) {
|
||||
// Convert JPG/JPEG to BMP (same approach as Epub)
|
||||
Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis());
|
||||
LOG_DBG("TXT", "Generating BMP from JPG cover image");
|
||||
FsFile coverJpg, coverBmp;
|
||||
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
||||
if (!Storage.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
||||
if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
@@ -156,16 +160,16 @@ bool Txt::generateCoverBmp() const {
|
||||
coverBmp.close();
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getCoverBmpPath().c_str());
|
||||
LOG_ERR("TXT", "Failed to generate BMP from JPG cover image");
|
||||
Storage.remove(getCoverBmpPath().c_str());
|
||||
} else {
|
||||
Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis());
|
||||
LOG_DBG("TXT", "Generated BMP from JPG cover image");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// PNG files are not supported (would need a PNG decoder)
|
||||
Serial.printf("[%lu] [TXT] Cover image format not supported (only BMP/JPG/JPEG)\n", millis());
|
||||
LOG_ERR("TXT", "Cover image format not supported (only BMP/JPG/JPEG)");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -175,7 +179,7 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
||||
if (!Storage.openFileForRead("TXT", filepath, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -28,6 +28,10 @@ class Txt {
|
||||
[[nodiscard]] bool generateCoverBmp() const;
|
||||
[[nodiscard]] std::string findCoverImage() const;
|
||||
|
||||
// Thumbnail paths (matching Epub/Xtc pattern for home screen covers)
|
||||
[[nodiscard]] std::string getThumbBmpPath() const;
|
||||
[[nodiscard]] std::string getThumbBmpPath(int height) const;
|
||||
|
||||
// Read content from file
|
||||
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
|
||||
};
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
#include "Xtc.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
bool Xtc::load() {
|
||||
Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str());
|
||||
LOG_DBG("XTC", "Loading XTC: %s", filepath.c_str());
|
||||
|
||||
// Initialize parser
|
||||
parser.reset(new xtc::XtcParser());
|
||||
@@ -19,43 +19,43 @@ bool Xtc::load() {
|
||||
// Open XTC file
|
||||
xtc::XtcError err = parser->open(filepath.c_str());
|
||||
if (err != xtc::XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to load: %s\n", millis(), xtc::errorToString(err));
|
||||
LOG_ERR("XTC", "Failed to load: %s", xtc::errorToString(err));
|
||||
parser.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
Serial.printf("[%lu] [XTC] Loaded XTC: %s (%lu pages)\n", millis(), filepath.c_str(), parser->getPageCount());
|
||||
LOG_DBG("XTC", "Loaded XTC: %s (%lu pages)", filepath.c_str(), parser->getPageCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Xtc::clearCache() const {
|
||||
if (!SdMan.exists(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [XTC] Cache does not exist, no action needed\n", millis());
|
||||
if (!Storage.exists(cachePath.c_str())) {
|
||||
LOG_DBG("XTC", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [XTC] Failed to clear cache\n", millis());
|
||||
if (!Storage.removeDir(cachePath.c_str())) {
|
||||
LOG_ERR("XTC", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [XTC] Cache cleared successfully\n", millis());
|
||||
LOG_DBG("XTC", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Xtc::setupCacheDir() const {
|
||||
if (SdMan.exists(cachePath.c_str())) {
|
||||
if (Storage.exists(cachePath.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create directories recursively
|
||||
for (size_t i = 1; i < cachePath.length(); i++) {
|
||||
if (cachePath[i] == '/') {
|
||||
SdMan.mkdir(cachePath.substr(0, i).c_str());
|
||||
Storage.mkdir(cachePath.substr(0, i).c_str());
|
||||
}
|
||||
}
|
||||
SdMan.mkdir(cachePath.c_str());
|
||||
Storage.mkdir(cachePath.c_str());
|
||||
}
|
||||
|
||||
std::string Xtc::getTitle() const {
|
||||
@@ -114,17 +114,17 @@ std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||
|
||||
bool Xtc::generateCoverBmp() const {
|
||||
// Already generated
|
||||
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
||||
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!loaded || !parser) {
|
||||
Serial.printf("[%lu] [XTC] Cannot generate cover BMP, file not loaded\n", millis());
|
||||
LOG_ERR("XTC", "Cannot generate cover BMP, file not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parser->getPageCount() == 0) {
|
||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
||||
LOG_ERR("XTC", "No pages in XTC file");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ bool Xtc::generateCoverBmp() const {
|
||||
// Get first page info for cover
|
||||
xtc::PageInfo pageInfo;
|
||||
if (!parser->getPageInfo(0, pageInfo)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
||||
LOG_DBG("XTC", "Failed to get first page info");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -152,22 +152,22 @@ bool Xtc::generateCoverBmp() const {
|
||||
}
|
||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||
if (!pageBuffer) {
|
||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
||||
LOG_ERR("XTC", "Failed to allocate page buffer (%lu bytes)", bitmapSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load first page (cover)
|
||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||
if (bytesRead == 0) {
|
||||
Serial.printf("[%lu] [XTC] Failed to load cover page\n", millis());
|
||||
LOG_ERR("XTC", "Failed to load cover page");
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create BMP file
|
||||
FsFile coverBmp;
|
||||
if (!SdMan.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to create cover BMP file\n", millis());
|
||||
if (!Storage.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) {
|
||||
LOG_DBG("XTC", "Failed to create cover BMP file");
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
@@ -297,7 +297,7 @@ bool Xtc::generateCoverBmp() const {
|
||||
coverBmp.close();
|
||||
free(pageBuffer);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generated cover BMP: %s\n", millis(), getCoverBmpPath().c_str());
|
||||
LOG_DBG("XTC", "Generated cover BMP: %s", getCoverBmpPath().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -306,17 +306,17 @@ std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_
|
||||
|
||||
bool Xtc::generateThumbBmp(int height) const {
|
||||
// Already generated
|
||||
if (SdMan.exists(getThumbBmpPath(height).c_str())) {
|
||||
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!loaded || !parser) {
|
||||
Serial.printf("[%lu] [XTC] Cannot generate thumb BMP, file not loaded\n", millis());
|
||||
LOG_ERR("XTC", "Cannot generate thumb BMP, file not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parser->getPageCount() == 0) {
|
||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
||||
LOG_ERR("XTC", "No pages in XTC file");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ bool Xtc::generateThumbBmp(int height) const {
|
||||
// Get first page info for cover
|
||||
xtc::PageInfo pageInfo;
|
||||
if (!parser->getPageInfo(0, pageInfo)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
||||
LOG_DBG("XTC", "Failed to get first page info");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -348,8 +348,8 @@ bool Xtc::generateThumbBmp(int height) const {
|
||||
// Copy cover.bmp to thumb.bmp
|
||||
if (generateCoverBmp()) {
|
||||
FsFile src, dst;
|
||||
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(height), dst)) {
|
||||
if (Storage.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
||||
if (Storage.openFileForWrite("XTC", getThumbBmpPath(height), dst)) {
|
||||
uint8_t buffer[512];
|
||||
while (src.available()) {
|
||||
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
||||
@@ -359,8 +359,8 @@ bool Xtc::generateThumbBmp(int height) const {
|
||||
}
|
||||
src.close();
|
||||
}
|
||||
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
|
||||
return SdMan.exists(getThumbBmpPath(height).c_str());
|
||||
LOG_DBG("XTC", "Copied cover to thumb (no scaling needed)");
|
||||
return Storage.exists(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -368,8 +368,8 @@ bool Xtc::generateThumbBmp(int height) const {
|
||||
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
||||
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
||||
pageInfo.height, thumbWidth, thumbHeight, scale);
|
||||
LOG_DBG("XTC", "Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)", pageInfo.width, pageInfo.height, thumbWidth,
|
||||
thumbHeight, scale);
|
||||
|
||||
// Allocate buffer for page data
|
||||
size_t bitmapSize;
|
||||
@@ -380,22 +380,22 @@ bool Xtc::generateThumbBmp(int height) const {
|
||||
}
|
||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||
if (!pageBuffer) {
|
||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
||||
LOG_ERR("XTC", "Failed to allocate page buffer (%lu bytes)", bitmapSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load first page (cover)
|
||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||
if (bytesRead == 0) {
|
||||
Serial.printf("[%lu] [XTC] Failed to load cover page for thumb\n", millis());
|
||||
LOG_ERR("XTC", "Failed to load cover page for thumb");
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
||||
FsFile thumbBmp;
|
||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
||||
if (!Storage.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) {
|
||||
LOG_DBG("XTC", "Failed to create thumb BMP file");
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
@@ -558,8 +558,7 @@ bool Xtc::generateThumbBmp(int height) const {
|
||||
thumbBmp.close();
|
||||
free(pageBuffer);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
|
||||
getThumbBmpPath(height).c_str());
|
||||
LOG_DBG("XTC", "Generated thumb BMP (%dx%d): %s", thumbWidth, thumbHeight, getThumbBmpPath(height).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#include "XtcParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -34,7 +34,7 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
}
|
||||
|
||||
// Open file
|
||||
if (!SdMan.openFileForRead("XTC", filepath, m_file)) {
|
||||
if (!Storage.openFileForRead("XTC", filepath, m_file)) {
|
||||
m_lastError = XtcError::FILE_NOT_FOUND;
|
||||
return m_lastError;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
// Read header
|
||||
m_lastError = readHeader();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read header: %s\n", millis(), errorToString(m_lastError));
|
||||
LOG_DBG("XTC", "Failed to read header: %s", errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
@@ -51,13 +51,13 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
if (m_header.hasMetadata) {
|
||||
m_lastError = readTitle();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
||||
LOG_DBG("XTC", "Failed to read title: %s", errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
m_lastError = readAuthor();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
||||
LOG_DBG("XTC", "Failed to read author: %s", errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
// Read page table
|
||||
m_lastError = readPageTable();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read page table: %s\n", millis(), errorToString(m_lastError));
|
||||
LOG_DBG("XTC", "Failed to read page table: %s", errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
@@ -74,14 +74,13 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
// Read chapters if present
|
||||
m_lastError = readChapters();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read chapters: %s\n", millis(), errorToString(m_lastError));
|
||||
LOG_DBG("XTC", "Failed to read chapters: %s", errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
m_isOpen = true;
|
||||
Serial.printf("[%lu] [XTC] Opened file: %s (%u pages, %dx%d)\n", millis(), filepath, m_header.pageCount,
|
||||
m_defaultWidth, m_defaultHeight);
|
||||
LOG_DBG("XTC", "Opened file: %s (%u pages, %dx%d)", filepath, m_header.pageCount, m_defaultWidth, m_defaultHeight);
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
@@ -106,8 +105,7 @@ XtcError XtcParser::readHeader() {
|
||||
|
||||
// Verify magic number (accept both XTC and XTCH)
|
||||
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
||||
Serial.printf("[%lu] [XTC] Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n", millis(), m_header.magic,
|
||||
XTC_MAGIC, XTCH_MAGIC);
|
||||
LOG_DBG("XTC", "Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)", m_header.magic, XTC_MAGIC, XTCH_MAGIC);
|
||||
return XtcError::INVALID_MAGIC;
|
||||
}
|
||||
|
||||
@@ -120,7 +118,7 @@ XtcError XtcParser::readHeader() {
|
||||
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
||||
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
||||
if (!validVersion) {
|
||||
Serial.printf("[%lu] [XTC] Unsupported version: %u.%u\n", millis(), m_header.versionMajor, m_header.versionMinor);
|
||||
LOG_DBG("XTC", "Unsupported version: %u.%u", m_header.versionMajor, m_header.versionMinor);
|
||||
return XtcError::INVALID_VERSION;
|
||||
}
|
||||
|
||||
@@ -129,9 +127,9 @@ XtcError XtcParser::readHeader() {
|
||||
return XtcError::CORRUPTED_HEADER;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [XTC] Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
||||
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
||||
m_header.pageCount, m_bitDepth);
|
||||
LOG_DBG("XTC", "Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u", m_header.magic,
|
||||
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
||||
m_header.pageCount, m_bitDepth);
|
||||
|
||||
return XtcError::OK;
|
||||
}
|
||||
@@ -146,7 +144,7 @@ XtcError XtcParser::readTitle() {
|
||||
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
||||
m_title = titleBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
|
||||
LOG_DBG("XTC", "Title: %s", m_title.c_str());
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
@@ -161,19 +159,19 @@ XtcError XtcParser::readAuthor() {
|
||||
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
||||
m_author = authorBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
||||
LOG_DBG("XTC", "Author: %s", m_author.c_str());
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
XtcError XtcParser::readPageTable() {
|
||||
if (m_header.pageTableOffset == 0) {
|
||||
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
||||
LOG_DBG("XTC", "Page table offset is 0, cannot read");
|
||||
return XtcError::CORRUPTED_HEADER;
|
||||
}
|
||||
|
||||
// Seek to page table
|
||||
if (!m_file.seek(m_header.pageTableOffset)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to seek to page table at %llu\n", millis(), m_header.pageTableOffset);
|
||||
LOG_DBG("XTC", "Failed to seek to page table at %llu", m_header.pageTableOffset);
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
@@ -184,7 +182,7 @@ XtcError XtcParser::readPageTable() {
|
||||
PageTableEntry entry;
|
||||
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
||||
if (bytesRead != sizeof(PageTableEntry)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read page table entry %u\n", millis(), i);
|
||||
LOG_DBG("XTC", "Failed to read page table entry %u", i);
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
@@ -201,7 +199,7 @@ XtcError XtcParser::readPageTable() {
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [XTC] Read %u page table entries\n", millis(), m_header.pageCount);
|
||||
LOG_DBG("XTC", "Read %u page table entries", m_header.pageCount);
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
@@ -307,7 +305,7 @@ XtcError XtcParser::readChapters() {
|
||||
}
|
||||
|
||||
m_hasChapters = !m_chapters.empty();
|
||||
Serial.printf("[%lu] [XTC] Chapters: %u\n", millis(), static_cast<unsigned int>(m_chapters.size()));
|
||||
LOG_DBG("XTC", "Chapters: %u", static_cast<unsigned int>(m_chapters.size()));
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
@@ -334,7 +332,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
||||
|
||||
// Seek to page data
|
||||
if (!m_file.seek(page.offset)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset);
|
||||
LOG_DBG("XTC", "Failed to seek to page %u at offset %lu", pageIndex, page.offset);
|
||||
m_lastError = XtcError::READ_ERROR;
|
||||
return 0;
|
||||
}
|
||||
@@ -343,7 +341,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
||||
XtgPageHeader pageHeader;
|
||||
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
||||
if (headerRead != sizeof(XtgPageHeader)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read page header for page %u\n", millis(), pageIndex);
|
||||
LOG_DBG("XTC", "Failed to read page header for page %u", pageIndex);
|
||||
m_lastError = XtcError::READ_ERROR;
|
||||
return 0;
|
||||
}
|
||||
@@ -351,8 +349,8 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
||||
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
||||
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
||||
if (pageHeader.magic != expectedMagic) {
|
||||
Serial.printf("[%lu] [XTC] Invalid page magic for page %u: 0x%08X (expected 0x%08X)\n", millis(), pageIndex,
|
||||
pageHeader.magic, expectedMagic);
|
||||
LOG_DBG("XTC", "Invalid page magic for page %u: 0x%08X (expected 0x%08X)", pageIndex, pageHeader.magic,
|
||||
expectedMagic);
|
||||
m_lastError = XtcError::INVALID_MAGIC;
|
||||
return 0;
|
||||
}
|
||||
@@ -370,7 +368,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
||||
|
||||
// Check buffer size
|
||||
if (bufferSize < bitmapSize) {
|
||||
Serial.printf("[%lu] [XTC] Buffer too small: need %u, have %u\n", millis(), bitmapSize, bufferSize);
|
||||
LOG_DBG("XTC", "Buffer too small: need %u, have %u", bitmapSize, bufferSize);
|
||||
m_lastError = XtcError::MEMORY_ERROR;
|
||||
return 0;
|
||||
}
|
||||
@@ -378,7 +376,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
||||
// Read bitmap data
|
||||
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
||||
if (bytesRead != bitmapSize) {
|
||||
Serial.printf("[%lu] [XTC] Page read error: expected %u, got %u\n", millis(), bitmapSize, bytesRead);
|
||||
LOG_DBG("XTC", "Page read error: expected %u, got %u", bitmapSize, bytesRead);
|
||||
m_lastError = XtcError::READ_ERROR;
|
||||
return 0;
|
||||
}
|
||||
@@ -444,7 +442,7 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
||||
|
||||
bool XtcParser::isValidXtcFile(const char* filepath) {
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("XTC", filepath, file)) {
|
||||
if (!Storage.openFileForRead("XTC", filepath, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "ZipFile.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <miniz.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -10,7 +10,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
||||
// Setup inflator
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
if (!inflator) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
|
||||
return false;
|
||||
}
|
||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||
@@ -23,7 +23,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
||||
free(inflator);
|
||||
|
||||
if (status != TINFL_STATUS_DONE) {
|
||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
||||
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -195,13 +195,13 @@ long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
|
||||
}
|
||||
|
||||
if (read != localHeaderSize) {
|
||||
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
|
||||
LOG_ERR("ZIP", "Something went wrong reading the local header");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||
Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis());
|
||||
LOG_ERR("ZIP", "Not a valid zip file header");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ bool ZipFile::loadZipDetails() {
|
||||
|
||||
const size_t fileSize = file.size();
|
||||
if (fileSize < 22) {
|
||||
Serial.printf("[%lu] [ZIP] File too small to be a valid zip\n", millis());
|
||||
LOG_ERR("ZIP", "File too small to be a valid zip");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -234,7 +234,7 @@ bool ZipFile::loadZipDetails() {
|
||||
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
||||
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
||||
if (!buffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for EOCD scan buffer\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -255,7 +255,7 @@ bool ZipFile::loadZipDetails() {
|
||||
}
|
||||
|
||||
if (foundOffset == -1) {
|
||||
Serial.printf("[%lu] [ZIP] EOCD signature not found in zip file\n", millis());
|
||||
LOG_ERR("ZIP", "EOCD signature not found in zip file");
|
||||
free(buffer);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
@@ -279,7 +279,7 @@ bool ZipFile::loadZipDetails() {
|
||||
}
|
||||
|
||||
bool ZipFile::open() {
|
||||
if (!SdMan.openFileForRead("ZIP", filePath, file)) {
|
||||
if (!Storage.openFileForRead("ZIP", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -407,7 +407,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
||||
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
||||
if (data == nullptr) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -422,7 +422,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
}
|
||||
|
||||
if (dataRead != inflatedDataSize) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to read data");
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -432,7 +432,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
// Read out deflated content from file
|
||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||
if (deflatedData == nullptr) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -445,7 +445,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
}
|
||||
|
||||
if (dataRead != deflatedDataSize) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
|
||||
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead);
|
||||
free(deflatedData);
|
||||
free(data);
|
||||
return nullptr;
|
||||
@@ -455,14 +455,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
free(deflatedData);
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to inflate file");
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Continue out of block with data set
|
||||
} else {
|
||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
||||
LOG_ERR("ZIP", "Unsupported compression method");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -498,7 +498,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
// no deflation, just read content
|
||||
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!buffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for buffer");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -509,7 +509,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
while (remaining > 0) {
|
||||
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
||||
if (dataRead == 0) {
|
||||
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
|
||||
LOG_ERR("ZIP", "Could not read more bytes");
|
||||
free(buffer);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
@@ -532,7 +532,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
// Setup inflator
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
if (!inflator) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -544,7 +544,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
// Setup file read buffer
|
||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!fileReadBuffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
||||
free(inflator);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
@@ -554,7 +554,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
|
||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||
if (!outputBuffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for dictionary");
|
||||
free(inflator);
|
||||
free(fileReadBuffer);
|
||||
if (!wasOpen) {
|
||||
@@ -605,7 +605,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
if (outBytes > 0) {
|
||||
processedOutputBytes += outBytes;
|
||||
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
|
||||
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -619,7 +619,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
|
||||
if (status < 0) {
|
||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
||||
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -630,8 +630,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
|
||||
if (status == TINFL_STATUS_DONE) {
|
||||
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
|
||||
inflatedDataSize);
|
||||
LOG_ERR("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -643,7 +642,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
|
||||
// If we get here, EOF reached without TINFL_STATUS_DONE
|
||||
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
|
||||
LOG_ERR("ZIP", "Unexpected EOF");
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -657,6 +656,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
close();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
||||
LOG_ERR("ZIP", "Unsupported compression method");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -32,6 +32,13 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
|
||||
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: Display only a rectangular region
|
||||
void HalDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
(void)mode; // EInkDisplay::displayWindow does not take mode yet
|
||||
einkDisplay.displayWindow(x, y, w, h, turnOffScreen);
|
||||
}
|
||||
|
||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ class HalDisplay {
|
||||
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// EXPERIMENTAL: Display only a rectangular region
|
||||
void displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// Power management
|
||||
void deepSleep();
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#include <HalGPIO.h>
|
||||
#include <SPI.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
void HalGPIO::begin() {
|
||||
inputMgr.begin();
|
||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
}
|
||||
|
||||
@@ -23,23 +21,6 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
||||
|
||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||
|
||||
void HalGPIO::startDeepSleep() {
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (inputMgr.isPressed(BTN_POWER)) {
|
||||
delay(50);
|
||||
inputMgr.update();
|
||||
}
|
||||
// Arm the wakeup trigger *after* the button is released
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
int HalGPIO::getBatteryPercentage() const {
|
||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||
return battery.readPercentage();
|
||||
}
|
||||
|
||||
bool HalGPIO::isUsbConnected() const {
|
||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||
return digitalRead(UART0_RXD) == HIGH;
|
||||
|
||||
@@ -38,12 +38,6 @@ class HalGPIO {
|
||||
bool wasAnyReleased() const;
|
||||
unsigned long getHeldTime() const;
|
||||
|
||||
// Setup wake up GPIO and enter deep sleep
|
||||
void startDeepSleep();
|
||||
|
||||
// Get battery percentage (range 0-100)
|
||||
int getBatteryPercentage() const;
|
||||
|
||||
// Check if USB is connected
|
||||
bool isUsbConnected() const;
|
||||
|
||||
|
||||
49
lib/hal/HalPowerManager.cpp
Normal file
49
lib/hal/HalPowerManager.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "HalPowerManager.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
|
||||
void HalPowerManager::begin() {
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
normalFreq = getCpuFrequencyMhz();
|
||||
}
|
||||
|
||||
void HalPowerManager::setPowerSaving(bool enabled) {
|
||||
if (normalFreq <= 0) {
|
||||
return; // invalid state
|
||||
}
|
||||
if (enabled && !isLowPower) {
|
||||
LOG_DBG("PWR", "Going to low-power mode");
|
||||
if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) {
|
||||
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!enabled && isLowPower) {
|
||||
LOG_DBG("PWR", "Restoring normal CPU frequency");
|
||||
if (!setCpuFrequencyMhz(normalFreq)) {
|
||||
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq);
|
||||
return;
|
||||
}
|
||||
}
|
||||
isLowPower = enabled;
|
||||
}
|
||||
|
||||
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||
delay(50);
|
||||
gpio.update();
|
||||
}
|
||||
// Arm the wakeup trigger *after* the button is released
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
int HalPowerManager::getBatteryPercentage() const {
|
||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||
return battery.readPercentage();
|
||||
}
|
||||
27
lib/hal/HalPowerManager.h
Normal file
27
lib/hal/HalPowerManager.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BatteryMonitor.h>
|
||||
#include <InputManager.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
|
||||
class HalPowerManager {
|
||||
int normalFreq = 0; // MHz
|
||||
bool isLowPower = false;
|
||||
|
||||
public:
|
||||
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||
|
||||
void begin();
|
||||
|
||||
// Control CPU frequency for power saving
|
||||
void setPowerSaving(bool enabled);
|
||||
|
||||
// Setup wake up GPIO and enter deep sleep
|
||||
void startDeepSleep(HalGPIO& gpio) const;
|
||||
|
||||
// Get battery percentage (range 0-100)
|
||||
int getBatteryPercentage() const;
|
||||
};
|
||||
65
lib/hal/HalStorage.cpp
Normal file
65
lib/hal/HalStorage.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "HalStorage.h"
|
||||
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#define SDCard SDCardManager::getInstance()
|
||||
|
||||
HalStorage HalStorage::instance;
|
||||
|
||||
HalStorage::HalStorage() {}
|
||||
|
||||
bool HalStorage::begin() { return SDCard.begin(); }
|
||||
|
||||
bool HalStorage::ready() const { return SDCard.ready(); }
|
||||
|
||||
std::vector<String> HalStorage::listFiles(const char* path, int maxFiles) { return SDCard.listFiles(path, maxFiles); }
|
||||
|
||||
String HalStorage::readFile(const char* path) { return SDCard.readFile(path); }
|
||||
|
||||
bool HalStorage::readFileToStream(const char* path, Print& out, size_t chunkSize) {
|
||||
return SDCard.readFileToStream(path, out, chunkSize);
|
||||
}
|
||||
|
||||
size_t HalStorage::readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes) {
|
||||
return SDCard.readFileToBuffer(path, buffer, bufferSize, maxBytes);
|
||||
}
|
||||
|
||||
bool HalStorage::writeFile(const char* path, const String& content) { return SDCard.writeFile(path, content); }
|
||||
|
||||
bool HalStorage::ensureDirectoryExists(const char* path) { return SDCard.ensureDirectoryExists(path); }
|
||||
|
||||
FsFile HalStorage::open(const char* path, const oflag_t oflag) { return SDCard.open(path, oflag); }
|
||||
|
||||
bool HalStorage::mkdir(const char* path, const bool pFlag) { return SDCard.mkdir(path, pFlag); }
|
||||
|
||||
bool HalStorage::exists(const char* path) { return SDCard.exists(path); }
|
||||
|
||||
bool HalStorage::remove(const char* path) { return SDCard.remove(path); }
|
||||
|
||||
bool HalStorage::rmdir(const char* path) { return SDCard.rmdir(path); }
|
||||
|
||||
bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) {
|
||||
return SDCard.openFileForRead(moduleName, path, file);
|
||||
}
|
||||
|
||||
bool HalStorage::openFileForRead(const char* moduleName, const std::string& path, FsFile& file) {
|
||||
return openFileForRead(moduleName, path.c_str(), file);
|
||||
}
|
||||
|
||||
bool HalStorage::openFileForRead(const char* moduleName, const String& path, FsFile& file) {
|
||||
return openFileForRead(moduleName, path.c_str(), file);
|
||||
}
|
||||
|
||||
bool HalStorage::openFileForWrite(const char* moduleName, const char* path, FsFile& file) {
|
||||
return SDCard.openFileForWrite(moduleName, path, file);
|
||||
}
|
||||
|
||||
bool HalStorage::openFileForWrite(const char* moduleName, const std::string& path, FsFile& file) {
|
||||
return openFileForWrite(moduleName, path.c_str(), file);
|
||||
}
|
||||
|
||||
bool HalStorage::openFileForWrite(const char* moduleName, const String& path, FsFile& file) {
|
||||
return openFileForWrite(moduleName, path.c_str(), file);
|
||||
}
|
||||
|
||||
bool HalStorage::removeDir(const char* path) { return SDCard.removeDir(path); }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user