Compare commits
58 Commits
1.0.0
...
97c33141bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c33141bd | ||
|
|
2a32d8a182 | ||
|
|
d6f38d4441 | ||
|
|
513d111634 | ||
|
|
ad9137cfdf | ||
|
|
5c80cface7 | ||
|
|
86d3774a8f | ||
|
|
7ba5978848 | ||
|
|
3d47c081f2 | ||
|
|
6702060960 | ||
|
|
0bc6747483 | ||
|
|
00666377de | ||
|
|
22b77edddf | ||
|
|
2e673c753d | ||
|
|
1a30826981 | ||
|
|
50e6ef9bd8 | ||
|
|
a616f42cb4 | ||
|
|
0508bfc1f7 | ||
|
|
6c3a615fac | ||
|
|
46c2109f1f | ||
|
|
5816ab2a47 | ||
|
|
2c0a105550 | ||
|
|
6e51afb977 | ||
|
|
cb24947477 | ||
|
|
7a385d78a4 | ||
|
|
0991782fb4 | ||
|
|
3ae1007cbe | ||
|
|
efb9b72e64 | ||
|
|
4a210823a8 | ||
|
|
f5b85f5ca1 | ||
|
|
7e93411f46 | ||
|
|
44452a42e9 | ||
|
|
0c2df24f5c | ||
|
|
3a12ca2725 | ||
|
|
98e6789626 | ||
|
|
b5d28a3a9c | ||
|
|
14ef625679 | ||
|
|
64d161e88b | ||
|
|
e73bb3213f | ||
|
|
6202bfd651 | ||
|
|
9b04c2ec76 | ||
|
|
ffddc2472b | ||
|
|
5765bbe821 | ||
|
|
b4b028be3a | ||
|
|
f34d7d2aac | ||
|
|
71769490fb | ||
|
|
cda0a3f898 | ||
|
|
7f40c3f477 | ||
|
|
a87eacc6ab | ||
|
|
1caad578fc | ||
|
|
5b90b68e99 | ||
|
|
67ddd60fce | ||
|
|
76908d38e1 | ||
|
|
e6f5fa43e6 | ||
|
|
e7e31ac487 | ||
|
|
9f78fd33e8 | ||
|
|
bd8132a260 | ||
|
|
f89ce514c8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nStrings.cpp
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
237
docs/i18n.md
Normal file
237
docs/i18n.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Internationalization (I18N)
|
||||
|
||||
This guide explains the multi-language support system in CrossPoint Reader.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- English
|
||||
- French
|
||||
- German
|
||||
- Portuguese
|
||||
- Spanish
|
||||
- Swedish
|
||||
- Czech
|
||||
- Russian
|
||||
|
||||
---
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
### Translation System Architecture
|
||||
|
||||
The I18N system uses **per-language YAML files** to maintain translations and a Python script to generate C++ code:
|
||||
|
||||
```
|
||||
lib/I18n/
|
||||
├── translations/ # One YAML file per language
|
||||
│ ├── english.yaml
|
||||
│ ├── spanish.yaml
|
||||
│ ├── french.yaml
|
||||
│ └── ...
|
||||
├── I18n.h
|
||||
├── I18n.cpp
|
||||
├── I18nKeys.h # Enums (auto-generated)
|
||||
├── I18nStrings.h # String array declarations (auto-generated)
|
||||
└── I18nStrings.cpp # String array definitions (auto-generated)
|
||||
|
||||
scripts/
|
||||
└── gen_i18n.py # Code generator script
|
||||
```
|
||||
|
||||
**Key principle:** All translations are managed in the YAML files under `lib/I18n/translations/`. The Python script generates the necessary C++ code automatically.
|
||||
|
||||
---
|
||||
|
||||
### YAML File Format
|
||||
|
||||
Each language has its own file in `lib/I18n/translations/` (e.g. `spanish.yaml`).
|
||||
|
||||
A file looks like this:
|
||||
|
||||
```yaml
|
||||
_language_name: "Español"
|
||||
_language_code: "SPANISH"
|
||||
_order: "1"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "BOOTING"
|
||||
STR_BROWSE_FILES: "Buscar archivos"
|
||||
```
|
||||
|
||||
**Metadata keys** (prefixed with `_`):
|
||||
- `_language_name` — Native display name shown to the user (e.g. "Français")
|
||||
- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier.
|
||||
- `_order` — Controls the position in the Language enum (English is always 0)
|
||||
|
||||
**Rules:**
|
||||
- Use UTF-8 encoding
|
||||
- Every line must follow the format: `KEY: "value"`
|
||||
- Keys must be valid C++ identifiers (uppercase, strats with STR_)
|
||||
- Keys must be unique within a file
|
||||
- String values must be quoted
|
||||
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
|
||||
|
||||
---
|
||||
|
||||
### Adding New Strings
|
||||
|
||||
To add a new translatable string:
|
||||
|
||||
#### 1. Edit the English YAML file
|
||||
|
||||
Add the key to `lib/I18n/translations/english.yaml`:
|
||||
|
||||
```yaml
|
||||
STR_MY_NEW_STRING: "My New String"
|
||||
```
|
||||
|
||||
Then add translations in each language file. If a key is missing from a
|
||||
language file, the generator will automatically use the English text as a
|
||||
fallback (and print a warning).
|
||||
|
||||
#### 2. Run the generator script
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically:
|
||||
- Fills missing translations from English
|
||||
- Updates the `StrId` enum in `I18nKeys.h`
|
||||
- Regenerates all language arrays in `I18nStrings.cpp`
|
||||
|
||||
#### 3. Use in code
|
||||
|
||||
```cpp
|
||||
#include <I18n.h>
|
||||
|
||||
// Using the tr() macro (recommended)
|
||||
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
|
||||
|
||||
// Using I18N.get() directly
|
||||
const char* text = I18N.get(StrId::STR_MY_NEW_STRING);
|
||||
```
|
||||
|
||||
**That's it!** No manual array synchronization needed.
|
||||
|
||||
---
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
To add support for a new language (e.g., Italian):
|
||||
|
||||
#### 1. Create a new YAML file
|
||||
|
||||
Create `lib/I18n/translations/italian.yaml`:
|
||||
|
||||
```yaml
|
||||
_language_name: "Italiano"
|
||||
_language_code: "ITALIAN"
|
||||
_order: "7"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "AVVIO"
|
||||
```
|
||||
|
||||
You only need to include the strings you have translations for. Missing
|
||||
keys will fall back to English automatically.
|
||||
|
||||
#### 2. Run the generator
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically updates all necessary code.
|
||||
|
||||
---
|
||||
|
||||
### Modifying Existing Translations
|
||||
|
||||
Simply edit the relevant YAML file and regenerate:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UTF-8 Encoding
|
||||
|
||||
The YAML files use UTF-8 encoding. Special characters are automatically converted to C++ UTF-8 hex sequences by the generator.
|
||||
|
||||
---
|
||||
|
||||
### I18N API Reference
|
||||
|
||||
```cpp
|
||||
// === Convenience Macros (Recommended) ===
|
||||
|
||||
// tr(id) - Get translated string without StrId:: prefix
|
||||
const char* text = tr(STR_SETTINGS_TITLE);
|
||||
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
|
||||
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
|
||||
|
||||
// I18N - Shorthand for I18n::getInstance()
|
||||
I18N.setLanguage(Language::SPANISH);
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// === Full API ===
|
||||
|
||||
// Get the singleton instance
|
||||
I18n& instance = I18n::getInstance();
|
||||
|
||||
// Get translated string (three equivalent ways)
|
||||
const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
|
||||
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
||||
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||
|
||||
// Set language
|
||||
I18N.setLanguage(Language::SPANISH);
|
||||
|
||||
// Get current language
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// Save language setting to file
|
||||
I18N.saveSettings();
|
||||
|
||||
// Load language setting from file
|
||||
I18N.loadSettings();
|
||||
|
||||
// Get character set for font subsetting (static method)
|
||||
const char* chars = I18n::getCharacterSet(Language::FRENCH);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Storage
|
||||
|
||||
Language settings are stored in:
|
||||
```
|
||||
/.crosspoint/language.bin
|
||||
```
|
||||
|
||||
This file contains:
|
||||
- Version byte
|
||||
- Current language selection (1 byte)
|
||||
|
||||
---
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
### For Developers (Adding Features)
|
||||
|
||||
1. Add new strings to `lib/I18n/translations/english.yaml`
|
||||
2. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
|
||||
3. Use the new `StrId` in your code
|
||||
4. Request translations from translators
|
||||
|
||||
### For Translators
|
||||
|
||||
1. Open the YAML file for your language in `lib/I18n/translations/`
|
||||
2. Add or update translations using the format `STR_KEY: "translated text"`
|
||||
3. Keep translations concise (E-ink space constraints)
|
||||
4. Make sure the file is in UTF-8 encoding
|
||||
5. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/` to verify
|
||||
6. Test on device or submit for review
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 54 KiB |
27
docs/translators.md
Normal file
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,9 +1,10 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Logging.h>
|
||||
#include <PngToBmpConverter.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include "Epub/parsers/ContainerParser.h"
|
||||
@@ -17,7 +18,7 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
|
||||
// Get file size without loading it all into heap
|
||||
if (!getItemSize(containerPath, &containerSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
|
||||
LOG_ERR("EBP", "Could not find or size META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,13 +30,13 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
|
||||
// Stream read (reusing your existing stream logic)
|
||||
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
||||
LOG_ERR("EBP", "Could not read META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the result
|
||||
if (containerParser.fullPath.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
||||
LOG_ERR("EBP", "Could not find valid rootfile in container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -46,28 +47,28 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
std::string contentOpfFilePath;
|
||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
||||
LOG_ERR("EBP", "Could not find content.opf in zip");
|
||||
return false;
|
||||
}
|
||||
|
||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
||||
LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str());
|
||||
|
||||
size_t contentOpfSize;
|
||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
||||
LOG_ERR("EBP", "Could not get size of content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||
if (!opfParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not setup content.opf parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
||||
LOG_ERR("EBP", "Could not read content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -76,6 +77,54 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
bookMetadata.author = opfParser.author;
|
||||
bookMetadata.language = opfParser.language;
|
||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||
|
||||
// Guide-based cover fallback: if no cover found via metadata/properties,
|
||||
// try extracting the image reference from the guide's cover page XHTML
|
||||
if (bookMetadata.coverItemHref.empty() && !opfParser.guideCoverPageHref.empty()) {
|
||||
LOG_DBG("EBP", "No cover from metadata, trying guide cover page: %s", opfParser.guideCoverPageHref.c_str());
|
||||
size_t coverPageSize;
|
||||
uint8_t* coverPageData = readItemContentsToBytes(opfParser.guideCoverPageHref, &coverPageSize, true);
|
||||
if (coverPageData) {
|
||||
const std::string coverPageHtml(reinterpret_cast<char*>(coverPageData), coverPageSize);
|
||||
free(coverPageData);
|
||||
|
||||
// Determine base path of the cover page for resolving relative image references
|
||||
std::string coverPageBase;
|
||||
const auto lastSlash = opfParser.guideCoverPageHref.rfind('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
coverPageBase = opfParser.guideCoverPageHref.substr(0, lastSlash + 1);
|
||||
}
|
||||
|
||||
// Search for image references: xlink:href="..." (SVG) and src="..." (img)
|
||||
std::string imageRef;
|
||||
for (const char* pattern : {"xlink:href=\"", "src=\""}) {
|
||||
auto pos = coverPageHtml.find(pattern);
|
||||
while (pos != std::string::npos) {
|
||||
pos += strlen(pattern);
|
||||
const auto endPos = coverPageHtml.find('"', pos);
|
||||
if (endPos != std::string::npos) {
|
||||
const auto ref = coverPageHtml.substr(pos, endPos - pos);
|
||||
// Check if it's an image file
|
||||
if (ref.length() >= 4) {
|
||||
const auto ext = ref.substr(ref.length() - 4);
|
||||
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") {
|
||||
imageRef = ref;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = coverPageHtml.find(pattern, pos);
|
||||
}
|
||||
if (!imageRef.empty()) break;
|
||||
}
|
||||
|
||||
if (!imageRef.empty()) {
|
||||
bookMetadata.coverItemHref = FsHelpers::normalisePath(coverPageBase + imageRef);
|
||||
LOG_DBG("EBP", "Found cover image from guide: %s", bookMetadata.coverItemHref.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||
|
||||
if (!opfParser.tocNcxPath.empty()) {
|
||||
@@ -90,27 +139,27 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
cssFiles = opfParser.cssFiles;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
LOG_DBG("EBP", "Successfully parsed content.opf");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNcxFile() const {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||
LOG_DBG("EBP", "No ncx file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||
|
||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||
FsFile tempNcxFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||
tempNcxFile.close();
|
||||
if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto ncxSize = tempNcxFile.size();
|
||||
@@ -118,14 +167,14 @@ bool Epub::parseTocNcxFile() const {
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||
|
||||
if (!ncxParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not setup toc ncx parser");
|
||||
tempNcxFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!ncxBuffer) {
|
||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||
tempNcxFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -136,7 +185,7 @@ bool Epub::parseTocNcxFile() const {
|
||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
|
||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||
free(ncxBuffer);
|
||||
tempNcxFile.close();
|
||||
return false;
|
||||
@@ -145,29 +194,29 @@ bool Epub::parseTocNcxFile() const {
|
||||
|
||||
free(ncxBuffer);
|
||||
tempNcxFile.close();
|
||||
SdMan.remove(tmpNcxPath.c_str());
|
||||
Storage.remove(tmpNcxPath.c_str());
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
||||
LOG_DBG("EBP", "Parsed TOC items");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNavFile() const {
|
||||
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||
if (tocNavItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No nav file specified\n", millis());
|
||||
LOG_DBG("EBP", "No nav file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing toc nav file: %s\n", millis(), tocNavItem.c_str());
|
||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||
|
||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||
FsFile tempNavFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||
tempNavFile.close();
|
||||
if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto navSize = tempNavFile.size();
|
||||
@@ -178,13 +227,13 @@ bool Epub::parseTocNavFile() const {
|
||||
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
||||
|
||||
if (!navParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup toc nav parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not setup toc nav parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!navBuffer) {
|
||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc nav parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -193,7 +242,7 @@ bool Epub::parseTocNavFile() const {
|
||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
Serial.printf("[%lu] [EBP] Could not process all toc nav data\n", millis());
|
||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||
free(navBuffer);
|
||||
tempNavFile.close();
|
||||
return false;
|
||||
@@ -202,98 +251,80 @@ bool Epub::parseTocNavFile() const {
|
||||
|
||||
free(navBuffer);
|
||||
tempNavFile.close();
|
||||
SdMan.remove(tmpNavPath.c_str());
|
||||
Storage.remove(tmpNavPath.c_str());
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis());
|
||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Epub::getCssRulesCache() const { return cachePath + "/css_rules.cache"; }
|
||||
|
||||
bool Epub::loadCssRulesFromCache() const {
|
||||
FsFile cssCacheFile;
|
||||
if (SdMan.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||
if (cssParser->loadFromCache(cssCacheFile)) {
|
||||
cssCacheFile.close();
|
||||
Serial.printf("[%lu] [EBP] Loaded CSS rules from cache\n", millis());
|
||||
return true;
|
||||
}
|
||||
cssCacheFile.close();
|
||||
Serial.printf("[%lu] [EBP] CSS cache invalid, reparsing\n", millis());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Epub::parseCssFiles() const {
|
||||
if (cssFiles.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis());
|
||||
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
|
||||
}
|
||||
|
||||
// Try to load from CSS cache first
|
||||
if (!loadCssRulesFromCache()) {
|
||||
// Cache miss - parse CSS files
|
||||
// See if we have a cached version of the CSS rules
|
||||
if (!cssParser->hasCache()) {
|
||||
// No cache yet - parse CSS files
|
||||
for (const auto& cssPath : cssFiles) {
|
||||
Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str());
|
||||
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||
|
||||
// Extract CSS file to temp location
|
||||
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||
FsFile tempCssFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||
Serial.printf("[%lu] [EBP] Could not create temp CSS file\n", millis());
|
||||
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||
LOG_ERR("EBP", "Could not create temp CSS file");
|
||||
continue;
|
||||
}
|
||||
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read CSS file: %s\n", millis(), cssPath.c_str());
|
||||
LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str());
|
||||
tempCssFile.close();
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
tempCssFile.close();
|
||||
|
||||
// Parse the CSS file
|
||||
if (!SdMan.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||
Serial.printf("[%lu] [EBP] Could not open temp CSS file for reading\n", millis());
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||
LOG_ERR("EBP", "Could not open temp CSS file for reading");
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
cssParser->loadFromStream(tempCssFile);
|
||||
tempCssFile.close();
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
}
|
||||
|
||||
// Save to cache for next time
|
||||
FsFile cssCacheFile;
|
||||
if (SdMan.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||
cssParser->saveToCache(cssCacheFile);
|
||||
cssCacheFile.close();
|
||||
if (!cssParser->saveToCache()) {
|
||||
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
||||
}
|
||||
cssParser->clear();
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files\n", millis(), cssParser->ruleCount(),
|
||||
cssFiles.size());
|
||||
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
||||
}
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||
LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str());
|
||||
|
||||
// Initialize spine/TOC cache
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||
cssParser.reset(new CssParser());
|
||||
cssParser.reset(new CssParser(cachePath));
|
||||
|
||||
// Try to load existing cache first
|
||||
if (bookMetadataCache->load()) {
|
||||
if (!skipLoadingCss && !loadCssRulesFromCache()) {
|
||||
Serial.printf("[%lu] [EBP] Warning: CSS rules cache not found, attempting to parse CSS files\n", millis());
|
||||
if (!skipLoadingCss && !cssParser->hasCache()) {
|
||||
LOG_DBG("EBP", "Warning: CSS rules cache not found, attempting to parse CSS files");
|
||||
// to get CSS file list
|
||||
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not parse content.opf from cached bookMetadata for CSS files\n", millis());
|
||||
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
||||
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||
}
|
||||
parseCssFiles();
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -303,14 +334,14 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
}
|
||||
|
||||
// Cache doesn't exist or is invalid, build it
|
||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||
LOG_DBG("EBP", "Cache not found, building spine/TOC cache");
|
||||
setupCacheDir();
|
||||
|
||||
const uint32_t indexingStart = millis();
|
||||
|
||||
// Begin building cache - stream entries to disk immediately
|
||||
if (!bookMetadataCache->beginWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||
LOG_ERR("EBP", "Could not begin writing cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -318,23 +349,23 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
const uint32_t opfStart = millis();
|
||||
BookMetadataCache::BookMetadata bookMetadata;
|
||||
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not begin writing content.opf pass");
|
||||
return false;
|
||||
}
|
||||
if (!parseContentOpf(bookMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
||||
LOG_ERR("EBP", "Could not parse content.opf");
|
||||
return false;
|
||||
}
|
||||
if (!bookMetadataCache->endContentOpfPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not end writing content.opf pass");
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
|
||||
LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart);
|
||||
|
||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||
const uint32_t tocStart = millis();
|
||||
if (!bookMetadataCache->beginTocPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not begin writing toc pass");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -342,50 +373,50 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
|
||||
// Try EPUB 3 nav document first (preferred)
|
||||
if (!tocNavItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Attempting to parse EPUB 3 nav document\n", millis());
|
||||
LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document");
|
||||
tocParsed = parseTocNavFile();
|
||||
}
|
||||
|
||||
// Fall back to NCX if nav parsing failed or wasn't available
|
||||
if (!tocParsed && !tocNcxItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Falling back to NCX TOC\n", millis());
|
||||
LOG_DBG("EBP", "Falling back to NCX TOC");
|
||||
tocParsed = parseTocNcxFile();
|
||||
}
|
||||
|
||||
if (!tocParsed) {
|
||||
Serial.printf("[%lu] [EBP] Warning: Could not parse any TOC format\n", millis());
|
||||
LOG_ERR("EBP", "Warning: Could not parse any TOC format");
|
||||
// Continue anyway - book will work without TOC
|
||||
}
|
||||
|
||||
if (!bookMetadataCache->endTocPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not end writing toc pass");
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
|
||||
LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart);
|
||||
|
||||
// Close the cache files
|
||||
if (!bookMetadataCache->endWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
||||
LOG_ERR("EBP", "Could not end writing cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build final book.bin
|
||||
const uint32_t buildStart = millis();
|
||||
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||
LOG_ERR("EBP", "Could not update mappings and sizes");
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
|
||||
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
|
||||
LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart);
|
||||
LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart);
|
||||
|
||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||
LOG_DBG("EBP", "Could not cleanup tmp files - ignoring");
|
||||
}
|
||||
|
||||
// Reload the cache from disk so it's in the correct state
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
if (!bookMetadataCache->load()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
||||
LOG_ERR("EBP", "Failed to reload cache after writing");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -394,31 +425,31 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
parseCssFiles();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::clearCache() const {
|
||||
if (!SdMan.exists(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
||||
if (!Storage.exists(cachePath.c_str())) {
|
||||
LOG_DBG("EPB", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
||||
if (!Storage.removeDir(cachePath.c_str())) {
|
||||
LOG_ERR("EPB", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
|
||||
LOG_DBG("EPB", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::setupCacheDir() const {
|
||||
if (SdMan.exists(cachePath.c_str())) {
|
||||
if (Storage.exists(cachePath.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
SdMan.mkdir(cachePath.c_str());
|
||||
Storage.mkdir(cachePath.c_str());
|
||||
}
|
||||
|
||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||
@@ -459,57 +490,89 @@ std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||
|
||||
bool Epub::generateCoverBmp(bool cropped) const {
|
||||
// Already generated, return true
|
||||
if (SdMan.exists(getCoverBmpPath(cropped).c_str())) {
|
||||
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||
LOG_ERR("EBP", "No known cover image");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
|
||||
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
|
||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
Storage.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getCoverBmpPath(cropped).c_str());
|
||||
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
|
||||
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
FsFile coverPng;
|
||||
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||
coverPng.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
coverPng.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
|
||||
coverPng.close();
|
||||
coverBmp.close();
|
||||
Storage.remove(coverPngTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
}
|
||||
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -518,36 +581,36 @@ std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb
|
||||
|
||||
bool Epub::generateThumbBmp(int height) const {
|
||||
// Already generated, return true
|
||||
if (SdMan.exists(getThumbBmpPath(height).c_str())) {
|
||||
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
||||
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
||||
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
|
||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
@@ -559,29 +622,62 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
THUMB_TARGET_HEIGHT);
|
||||
coverJpg.close();
|
||||
thumbBmp.close();
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
Storage.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getThumbBmpPath(height).c_str());
|
||||
LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image");
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
||||
success ? "yes" : "no");
|
||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
FsFile coverPng;
|
||||
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||
coverPng.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
coverPng.close();
|
||||
return false;
|
||||
}
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
const bool success =
|
||||
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
||||
coverPng.close();
|
||||
thumbBmp.close();
|
||||
Storage.remove(coverPngTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
|
||||
// Write an empty bmp file to avoid generation attempts in the future
|
||||
FsFile thumbBmp;
|
||||
SdMan.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||
thumbBmp.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
if (itemHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -589,7 +685,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
||||
|
||||
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||
if (!content) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
||||
LOG_DBG("EBP", "Failed to read item %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -598,7 +694,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
||||
|
||||
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
||||
if (itemHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -622,12 +718,12 @@ size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return get
|
||||
|
||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "getSpineItem called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||
LOG_ERR("EBP", "getSpineItem index:%d is out of range", spineIndex);
|
||||
return bookMetadataCache->getSpineEntry(0);
|
||||
}
|
||||
|
||||
@@ -636,12 +732,12 @@ BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
|
||||
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
||||
LOG_DBG("EBP", "getTocItem called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
||||
LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -659,18 +755,18 @@ int Epub::getTocItemsCount() const {
|
||||
// work out the section index for a toc index
|
||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "getSpineIndexForTocIndex called but cache not loaded");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
||||
LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||
if (spineIndex < 0) {
|
||||
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
||||
LOG_DBG("EBP", "Section not found for TOC index %d", tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -688,14 +784,13 @@ size_t Epub::getBookSize() const {
|
||||
|
||||
int Epub::getSpineIndexForTextReference() const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded");
|
||||
return 0;
|
||||
}
|
||||
Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(),
|
||||
bookMetadataCache->coreMetadata.coverItemHref.size(),
|
||||
bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||
LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s",
|
||||
bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||
|
||||
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||
@@ -705,13 +800,13 @@ int Epub::getSpineIndexForTextReference() const {
|
||||
// loop through spine items to get the correct index matching the text href
|
||||
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
|
||||
LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(),
|
||||
i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
|
||||
LOG_DBG("EBP", "Section not found for text reference");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
@@ -73,5 +71,5 @@ 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(); }
|
||||
};
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
@@ -25,6 +25,29 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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 +59,9 @@ 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));
|
||||
// Use getTag() method to determine type
|
||||
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||
|
||||
if (!el->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -59,8 +83,11 @@ 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_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#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_PageImage = 2, // New tag
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
@@ -19,6 +22,7 @@ class PageElement {
|
||||
virtual ~PageElement() = default;
|
||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||
virtual bool serialize(FsFile& file) = 0;
|
||||
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||
};
|
||||
|
||||
// a line from a block element
|
||||
@@ -30,9 +34,23 @@ class PageLine final : public PageElement {
|
||||
: PageElement(xPos, yPos), block(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_PageLine; }
|
||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
// New PageImage class
|
||||
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);
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
// the list of block index and line numbers on this page
|
||||
@@ -40,4 +58,10 @@ 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; });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +32,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);
|
||||
|
||||
@@ -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,57 @@ 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 +227,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 +238,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
174
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
174
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "../converters/DitherUtils.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
|
||||
// Cache file format:
|
||||
// - uint16_t width
|
||||
// - uint16_t height
|
||||
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||
|
||||
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||
: imagePath(imagePath), width(width), height(height) {}
|
||||
|
||||
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||
|
||||
namespace {
|
||||
|
||||
std::string getCachePath(const std::string& imagePath) {
|
||||
// Replace extension with .pxc (pixel cache)
|
||||
size_t dotPos = imagePath.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
return imagePath.substr(0, dotPos) + ".pxc";
|
||||
}
|
||||
return imagePath + ".pxc";
|
||||
}
|
||||
|
||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
FsFile cacheFile;
|
||||
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||
expectedHeight);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||
expectedWidth = cachedWidth;
|
||||
expectedHeight = cachedHeight;
|
||||
|
||||
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||
|
||||
// Read and render row by row to minimize memory usage
|
||||
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
if (!rowBuffer) {
|
||||
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int destY = y + row;
|
||||
for (int col = 0; col < cachedWidth; col++) {
|
||||
int byteIdx = col / 4;
|
||||
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
|
||||
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
||||
}
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
LOG_DBG("IMG", "Cache render complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Bounds check render position using logical screen dimensions
|
||||
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||
screenHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
return; // Successfully rendered from cache
|
||||
}
|
||||
|
||||
// No cache - need to decode the image
|
||||
// Check if image file exists
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
file.close();
|
||||
|
||||
if (fileSize == 0) {
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||
|
||||
RenderConfig config;
|
||||
config.x = x;
|
||||
config.y = y;
|
||||
config.maxWidth = width;
|
||||
config.maxHeight = height;
|
||||
config.useGrayscale = true;
|
||||
config.useDithering = true;
|
||||
config.performanceMode = false;
|
||||
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||
config.cachePath = cachePath; // Enable caching during decode
|
||||
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Decode successful");
|
||||
}
|
||||
|
||||
bool ImageBlock::serialize(FsFile& file) {
|
||||
serialization::writeString(file, imagePath);
|
||||
serialization::writePod(file, width);
|
||||
serialization::writePod(file, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
||||
std::string path;
|
||||
serialization::readString(file, path);
|
||||
int16_t w, h;
|
||||
serialization::readPod(file, w);
|
||||
serialization::readPod(file, h);
|
||||
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||
}
|
||||
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,13 +1,14 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
||||
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||
const char* visiblePtr = w.c_str() + 3;
|
||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, std::string("\xe2\x80\x83").c_str());
|
||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83");
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||
startX = wordX + prefixWidth;
|
||||
underlineWidth = visibleWidth;
|
||||
@@ -49,8 +50,8 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
|
||||
// Sanity check: prevent allocation of unreasonably large lists (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,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <EpdFontFamily.h>
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
@@ -28,7 +28,6 @@ class TextBlock final : public Block {
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
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 <Logging.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||
|
||||
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||
std::string ext = imagePath;
|
||||
size_t dotPos = ext.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
ext = ext.substr(dotPos);
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
} else {
|
||||
ext = "";
|
||||
}
|
||||
|
||||
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!jpegDecoder) {
|
||||
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||
}
|
||||
return jpegDecoder.get();
|
||||
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!pngDecoder) {
|
||||
pngDecoder.reset(new PngToFramebufferConverter());
|
||||
}
|
||||
return pngDecoder.get();
|
||||
}
|
||||
|
||||
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||
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;
|
||||
};
|
||||
17
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
17
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
#include <Logging.h>
|
||||
|
||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||
if (width * height > MAX_SOURCE_PIXELS) {
|
||||
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
||||
format.c_str(), MAX_SOURCE_PIXELS);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||
imagePath.c_str());
|
||||
}
|
||||
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);
|
||||
};
|
||||
297
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
297
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
#include <picojpeg.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
struct JpegContext {
|
||||
FsFile& file;
|
||||
uint8_t buffer[512];
|
||||
size_t bufferPos;
|
||||
size_t bufferFilled;
|
||||
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
|
||||
};
|
||||
|
||||
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JpegContext context(file);
|
||||
pjpeg_image_info_t imageInfo;
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
file.close();
|
||||
|
||||
if (status != 0) {
|
||||
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = imageInfo.m_width;
|
||||
out.height = imageInfo.m_height;
|
||||
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JpegContext context(file);
|
||||
pjpeg_image_info_t imageInfo;
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
if (status != 0) {
|
||||
LOG_ERR("JPG", "picojpeg init failed: %d", status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate output dimensions
|
||||
int destWidth, destHeight;
|
||||
float scale;
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||
destWidth = config.maxWidth;
|
||||
destHeight = config.maxHeight;
|
||||
scale = (float)destWidth / imageInfo.m_width;
|
||||
} else {
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth)
|
||||
? (float)config.maxWidth / imageInfo.m_width
|
||||
: 1.0f;
|
||||
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
|
||||
? (float)config.maxHeight / imageInfo.m_height
|
||||
: 1.0f;
|
||||
scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (scale > 1.0f) scale = 1.0f;
|
||||
|
||||
destWidth = (int)(imageInfo.m_width * scale);
|
||||
destHeight = (int)(imageInfo.m_height * scale);
|
||||
}
|
||||
|
||||
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
||||
|
||||
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
|
||||
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Allocate pixel cache if cachePath is provided
|
||||
PixelCache cache;
|
||||
bool caching = !config.cachePath.empty();
|
||||
if (caching) {
|
||||
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||
caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
int mcuX = 0;
|
||||
int mcuY = 0;
|
||||
|
||||
while (mcuY < imageInfo.m_MCUSPerCol) {
|
||||
status = pjpeg_decode_mcu();
|
||||
if (status == PJPG_NO_MORE_BLOCKS) {
|
||||
break;
|
||||
}
|
||||
if (status != 0) {
|
||||
LOG_ERR("JPG", "MCU decode failed: %d", status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Source position in image coordinates
|
||||
int srcStartX = mcuX * imageInfo.m_MCUWidth;
|
||||
int srcStartY = mcuY * imageInfo.m_MCUHeight;
|
||||
|
||||
switch (imageInfo.m_scanType) {
|
||||
case PJPG_GRAYSCALE:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH1V1:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH2V1:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 16; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockIndex = (col < 8) ? 0 : 1;
|
||||
int pixelIndex = row * 8 + (col % 8);
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH1V2:
|
||||
for (int row = 0; row < 16; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockIndex = (row < 8) ? 0 : 1;
|
||||
int pixelIndex = (row % 8) * 8 + col;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH2V2:
|
||||
for (int row = 0; row < 16; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 16; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockX = (col < 8) ? 0 : 1;
|
||||
int blockY = (row < 8) ? 0 : 1;
|
||||
int blockIndex = blockY * 2 + blockX;
|
||||
int pixelIndex = (row % 8) * 8 + (col % 8);
|
||||
int blockOffset = blockIndex * 64;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
mcuX++;
|
||||
if (mcuX >= imageInfo.m_MCUSPerRow) {
|
||||
mcuX = 0;
|
||||
mcuY++;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DBG("JPG", "Decoding complete");
|
||||
file.close();
|
||||
|
||||
// Write cache file if caching was enabled
|
||||
if (caching) {
|
||||
cache.writeToFile(config.cachePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
||||
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
|
||||
|
||||
if (context->bufferPos >= context->bufferFilled) {
|
||||
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
|
||||
if (readCount <= 0) {
|
||||
*pBytes_actually_read = 0;
|
||||
return 0;
|
||||
}
|
||||
context->bufferFilled = readCount;
|
||||
context->bufferPos = 0;
|
||||
}
|
||||
|
||||
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
|
||||
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
|
||||
|
||||
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
|
||||
context->bufferPos += bytesToCopy;
|
||||
*pBytes_actually_read = bytesToCopy;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".jpg" || ext == ".jpeg");
|
||||
}
|
||||
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);
|
||||
};
|
||||
82
lib/Epub/Epub/converters/PixelCache.h
Normal file
82
lib/Epub/Epub/converters/PixelCache.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Cache buffer for storing 2-bit pixels (4 levels) during decode.
|
||||
// Packs 4 pixels per byte, MSB first.
|
||||
struct PixelCache {
|
||||
uint8_t* buffer;
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
int originX; // config.x - to convert screen coords to cache coords
|
||||
int originY; // config.y
|
||||
|
||||
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
|
||||
PixelCache(const PixelCache&) = delete;
|
||||
PixelCache& operator=(const PixelCache&) = delete;
|
||||
|
||||
static constexpr size_t MAX_CACHE_BYTES = 256 * 1024; // 256KB limit for embedded targets
|
||||
|
||||
bool allocate(int w, int h, int ox, int oy) {
|
||||
width = w;
|
||||
height = h;
|
||||
originX = ox;
|
||||
originY = oy;
|
||||
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
size_t bufferSize = (size_t)bytesPerRow * h;
|
||||
if (bufferSize > MAX_CACHE_BYTES) {
|
||||
LOG_ERR("IMG", "Cache buffer too large: %d bytes for %dx%d (limit %d)", bufferSize, w, h, MAX_CACHE_BYTES);
|
||||
return false;
|
||||
}
|
||||
buffer = (uint8_t*)malloc(bufferSize);
|
||||
if (buffer) {
|
||||
memset(buffer, 0, bufferSize);
|
||||
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
|
||||
}
|
||||
return buffer != nullptr;
|
||||
}
|
||||
|
||||
void setPixel(int screenX, int screenY, uint8_t value) {
|
||||
if (!buffer) return;
|
||||
int localX = screenX - originX;
|
||||
int localY = screenY - originY;
|
||||
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
|
||||
|
||||
int byteIdx = localY * bytesPerRow + localX / 4;
|
||||
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
|
||||
bool writeToFile(const std::string& cachePath) {
|
||||
if (!buffer) return false;
|
||||
|
||||
FsFile cacheFile;
|
||||
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t w = width;
|
||||
uint16_t h = height;
|
||||
cacheFile.write(&w, 2);
|
||||
cacheFile.write(&h, 2);
|
||||
cacheFile.write(buffer, bytesPerRow * height);
|
||||
cacheFile.close();
|
||||
|
||||
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
|
||||
return true;
|
||||
}
|
||||
|
||||
~PixelCache() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
362
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
362
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,362 @@
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <PNGdec.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Context struct passed through PNGdec callbacks to avoid global mutable state.
|
||||
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
|
||||
struct PngContext {
|
||||
GfxRenderer* renderer;
|
||||
const RenderConfig* config;
|
||||
int screenWidth;
|
||||
int screenHeight;
|
||||
|
||||
// Scaling state
|
||||
float scale;
|
||||
int srcWidth;
|
||||
int srcHeight;
|
||||
int dstWidth;
|
||||
int dstHeight;
|
||||
int lastDstY; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
PixelCache cache;
|
||||
bool caching;
|
||||
|
||||
uint8_t* grayLineBuffer;
|
||||
|
||||
PngContext()
|
||||
: renderer(nullptr),
|
||||
config(nullptr),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
scale(1.0f),
|
||||
srcWidth(0),
|
||||
srcHeight(0),
|
||||
dstWidth(0),
|
||||
dstHeight(0),
|
||||
lastDstY(-1),
|
||||
caching(false),
|
||||
grayLineBuffer(nullptr) {}
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the FsFile*,
|
||||
// avoiding the need for global file state.
|
||||
void* pngOpenWithHandle(const char* filename, int32_t* size) {
|
||||
FsFile* f = new FsFile();
|
||||
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
|
||||
delete f;
|
||||
return nullptr;
|
||||
}
|
||||
*size = f->size();
|
||||
return f;
|
||||
}
|
||||
|
||||
void pngCloseWithHandle(void* handle) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(handle);
|
||||
if (f) {
|
||||
f->close();
|
||||
delete f;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||
if (!f) return 0;
|
||||
return f->read(pBuf, len);
|
||||
}
|
||||
|
||||
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||
if (!f) return -1;
|
||||
return f->seek(pos);
|
||||
}
|
||||
|
||||
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
|
||||
// We heap-allocate it on demand rather than using a static instance, so this memory
|
||||
// is only consumed while actually decoding/querying PNG images. This is critical on
|
||||
// the ESP32-C3 where total RAM is ~320 KB.
|
||||
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||
|
||||
// Convert entire source line to grayscale with alpha blending to white background.
|
||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
memcpy(grayLine, pPixels, width);
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_INDEXED:
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = pPixels[x];
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &palette[pPixels[x] * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t gray = pPixels[x * 2];
|
||||
uint8_t alpha = pPixels[x * 2 + 1];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 4];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
memset(grayLine, 128, width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * ctx->scale);
|
||||
|
||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||
if (dstY == ctx->lastDstY) return 1;
|
||||
ctx->lastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= ctx->dstHeight) return 1;
|
||||
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) return 1;
|
||||
|
||||
// Convert entire source line to grayscale (improves cache locality)
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||
pDraw->iHasAlpha);
|
||||
|
||||
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||
int dstWidth = ctx->dstWidth;
|
||||
int outXBase = ctx->config->x;
|
||||
int screenWidth = ctx->screenWidth;
|
||||
bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||
return false;
|
||||
}
|
||||
|
||||
PNG* png = new (std::nothrow) PNG();
|
||||
if (!png) {
|
||||
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
nullptr);
|
||||
|
||||
if (rc != 0) {
|
||||
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = png->getWidth();
|
||||
out.height = png->getHeight();
|
||||
|
||||
png->close();
|
||||
delete png;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
|
||||
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
|
||||
PNG* png = new (std::nothrow) PNG();
|
||||
if (!png) {
|
||||
LOG_ERR("PNG", "Failed to allocate PNG decoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
PngContext ctx;
|
||||
ctx.renderer = &renderer;
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
ctx.screenHeight = renderer.getScreenHeight();
|
||||
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
pngDrawCallback);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate output dimensions
|
||||
ctx.srcWidth = png->getWidth();
|
||||
ctx.srcHeight = png->getHeight();
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||
ctx.dstWidth = config.maxWidth;
|
||||
ctx.dstHeight = config.maxHeight;
|
||||
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||
} else {
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||
|
||||
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||
ctx.scale, png->getBpp());
|
||||
|
||||
if (png->getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||
}
|
||||
|
||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate cache buffer using SCALED dimensions
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long decodeStart = millis();
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
free(ctx.grayLineBuffer);
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
png->close();
|
||||
delete png;
|
||||
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
|
||||
|
||||
// Write cache file if caching was enabled and buffer was allocated
|
||||
if (ctx.caching) {
|
||||
ctx.cache.writeToFile(config.cachePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".png");
|
||||
}
|
||||
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,95 @@ CssLength CssParser::interpretLength(const std::string& val) {
|
||||
|
||||
return CssLength{numericValue, unit};
|
||||
}
|
||||
// Declaration parsing
|
||||
|
||||
int8_t CssParser::interpretSpacing(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
if (v.empty()) return 0;
|
||||
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;
|
||||
|
||||
// For spacing, we convert to "lines" (discrete units for e-ink)
|
||||
// 1em ≈ 1 line, percentages based on ~30 lines per page
|
||||
normalizedInto(decl.substr(0, colonPos), propNameBuf);
|
||||
normalizedInto(decl.substr(colonPos + 1), propValueBuf);
|
||||
|
||||
float multiplier = 0.0f;
|
||||
size_t unitStart = v.size();
|
||||
if (propNameBuf.empty() || propValueBuf.empty()) return;
|
||||
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
const char c = v[i];
|
||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||
unitStart = i;
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 +322,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 +363,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 +563,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 +632,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 +653,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 +671,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 +679,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
||||
selector.resize(selectorLen);
|
||||
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||
rulesBySelector_.clear();
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -629,24 +689,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 +732,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 +740,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 +760,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);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -8,25 +8,28 @@
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-it.trie.h"
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// 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);
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 5>;
|
||||
using EntryArray = std::array<LanguageEntry, 6>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
{"german", "de", &germanHyphenator},
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
{"spanish", "es", &spanishHyphenator}}};
|
||||
{"spanish", "es", &spanishHyphenator},
|
||||
{"italian", "it", &italianHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,22 @@
|
||||
#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 "../../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]);
|
||||
@@ -155,30 +160,125 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
std::string alt = "[Image]";
|
||||
std::string src;
|
||||
std::string alt;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
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());
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||
@@ -359,6 +459,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);
|
||||
@@ -386,13 +508,29 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
// 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());
|
||||
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);
|
||||
|
||||
@@ -477,12 +615,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 +641,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 +653,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 +665,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 +710,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
#include <memory>
|
||||
|
||||
#include "../ParsedText.h"
|
||||
#include "../blocks/ImageBlock.h"
|
||||
#include "../blocks/TextBlock.h"
|
||||
#include "../css/CssParser.h"
|
||||
#include "../css/CssStyle.h"
|
||||
|
||||
class Page;
|
||||
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 +46,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 {
|
||||
@@ -64,18 +70,21 @@ class ChapterHtmlSlimParser {
|
||||
// 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 +96,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;
|
||||
@@ -295,23 +296,22 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
// parse the guide
|
||||
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
||||
std::string type;
|
||||
std::string textHref;
|
||||
std::string guideHref;
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "type") == 0) {
|
||||
type = atts[i + 1];
|
||||
if (type == "text" || type == "start") {
|
||||
continue;
|
||||
} else {
|
||||
Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str());
|
||||
break;
|
||||
}
|
||||
} else if (strcmp(atts[i], "href") == 0) {
|
||||
textHref = FsHelpers::normalisePath(self->baseContentPath + atts[i + 1]);
|
||||
guideHref = FsHelpers::normalisePath(self->baseContentPath + atts[i + 1]);
|
||||
}
|
||||
}
|
||||
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());
|
||||
self->textReferenceHref = textHref;
|
||||
if (!guideHref.empty()) {
|
||||
if (type == "text" || (type == "start" && !self->textReferenceHref.empty())) {
|
||||
LOG_DBG("COF", "Found %s reference in guide: %s", type.c_str(), guideHref.c_str());
|
||||
self->textReferenceHref = guideHref;
|
||||
} else if ((type == "cover" || type == "cover-page") && self->guideCoverPageHref.empty()) {
|
||||
LOG_DBG("COF", "Found cover reference in guide: %s", guideHref.c_str());
|
||||
self->guideCoverPageHref = guideHref;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -326,6 +326,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;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ class ContentOpfParser final : public Print {
|
||||
std::string tocNcxPath;
|
||||
std::string tocNavPath; // EPUB 3 nav document path
|
||||
std::string coverItemHref;
|
||||
std::string guideCoverPageHref; // Guide reference with type="cover" or "cover-page" (points to XHTML wrapper)
|
||||
std::string textReferenceHref;
|
||||
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
|
||||
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 +101,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 +134,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,8 +420,8 @@ 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());
|
||||
@@ -430,7 +431,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
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 +440,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;
|
||||
@@ -458,7 +459,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -521,7 +522,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 +531,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;
|
||||
@@ -588,7 +589,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,7 +656,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -709,7 +710,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 +719,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 +733,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 +742,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 +751,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 +765,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);
|
||||
@@ -872,8 +873,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 +882,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 +891,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 +918,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 +930,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 +952,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ class GfxRenderer {
|
||||
|
||||
// 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()
|
||||
381
lib/I18n/I18nKeys.h
Normal file
381
lib/I18n/I18nKeys.h
Normal file
@@ -0,0 +1,381 @@
|
||||
#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,
|
||||
// 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"
|
||||
317
lib/I18n/translations/english.yaml
Normal file
317
lib/I18n/translations/english.yaml
Normal file
@@ -0,0 +1,317 @@
|
||||
_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"
|
||||
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/russian.yaml
Normal file
317
lib/I18n/translations/russian.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;
|
||||
|
||||
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
@@ -0,0 +1,858 @@
|
||||
#include "PngToBmpConverter.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <miniz.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "BitmapHelpers.h"
|
||||
|
||||
// ============================================================================
|
||||
// IMAGE PROCESSING OPTIONS - Same as JpegToBmpConverter for consistency
|
||||
// ============================================================================
|
||||
constexpr bool USE_8BIT_OUTPUT = false;
|
||||
constexpr bool USE_ATKINSON = true;
|
||||
constexpr bool USE_FLOYD_STEINBERG = false;
|
||||
constexpr bool USE_PRESCALE = true;
|
||||
constexpr int TARGET_MAX_WIDTH = 480;
|
||||
constexpr int TARGET_MAX_HEIGHT = 800;
|
||||
// ============================================================================
|
||||
|
||||
// PNG constants
|
||||
static constexpr uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10};
|
||||
|
||||
// PNG color types
|
||||
enum PngColorType : uint8_t {
|
||||
PNG_COLOR_GRAYSCALE = 0,
|
||||
PNG_COLOR_RGB = 2,
|
||||
PNG_COLOR_PALETTE = 3,
|
||||
PNG_COLOR_GRAYSCALE_ALPHA = 4,
|
||||
PNG_COLOR_RGBA = 6,
|
||||
};
|
||||
|
||||
// PNG filter types
|
||||
enum PngFilter : uint8_t {
|
||||
PNG_FILTER_NONE = 0,
|
||||
PNG_FILTER_SUB = 1,
|
||||
PNG_FILTER_UP = 2,
|
||||
PNG_FILTER_AVERAGE = 3,
|
||||
PNG_FILTER_PAETH = 4,
|
||||
};
|
||||
|
||||
// Read a big-endian 32-bit value from file
|
||||
static bool readBE32(FsFile& file, uint32_t& value) {
|
||||
uint8_t buf[4];
|
||||
if (file.read(buf, 4) != 4) return false;
|
||||
value = (static_cast<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
|
||||
(static_cast<uint32_t>(buf[2]) << 8) | buf[3];
|
||||
return true;
|
||||
}
|
||||
|
||||
// BMP writing helpers (same as JpegToBmpConverter)
|
||||
inline void write16(Print& out, const uint16_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32(Print& out, const uint32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32Signed(Print& out, const int32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
||||
const int bytesPerRow = (width + 3) / 4 * 4;
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t paletteSize = 256 * 4;
|
||||
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
|
||||
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize);
|
||||
write32(bmpOut, 0);
|
||||
write32(bmpOut, 14 + 40 + paletteSize);
|
||||
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height);
|
||||
write16(bmpOut, 1);
|
||||
write16(bmpOut, 8);
|
||||
write32(bmpOut, 0);
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835);
|
||||
write32(bmpOut, 2835);
|
||||
write32(bmpOut, 256);
|
||||
write32(bmpOut, 256);
|
||||
|
||||
for (int i = 0; i < 256; i++) {
|
||||
bmpOut.write(static_cast<uint8_t>(i));
|
||||
bmpOut.write(static_cast<uint8_t>(i));
|
||||
bmpOut.write(static_cast<uint8_t>(i));
|
||||
bmpOut.write(static_cast<uint8_t>(0));
|
||||
}
|
||||
}
|
||||
|
||||
static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
|
||||
const int bytesPerRow = (width + 31) / 32 * 4;
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t fileSize = 62 + imageSize;
|
||||
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize);
|
||||
write32(bmpOut, 0);
|
||||
write32(bmpOut, 62);
|
||||
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height);
|
||||
write16(bmpOut, 1);
|
||||
write16(bmpOut, 1);
|
||||
write32(bmpOut, 0);
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835);
|
||||
write32(bmpOut, 2835);
|
||||
write32(bmpOut, 2);
|
||||
write32(bmpOut, 2);
|
||||
|
||||
uint8_t palette[8] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
|
||||
for (const uint8_t i : palette) {
|
||||
bmpOut.write(i);
|
||||
}
|
||||
}
|
||||
|
||||
static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
|
||||
const int bytesPerRow = (width * 2 + 31) / 32 * 4;
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t fileSize = 70 + imageSize;
|
||||
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize);
|
||||
write32(bmpOut, 0);
|
||||
write32(bmpOut, 70);
|
||||
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height);
|
||||
write16(bmpOut, 1);
|
||||
write16(bmpOut, 2);
|
||||
write32(bmpOut, 0);
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835);
|
||||
write32(bmpOut, 2835);
|
||||
write32(bmpOut, 4);
|
||||
write32(bmpOut, 4);
|
||||
|
||||
uint8_t palette[16] = {0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x00,
|
||||
0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
|
||||
for (const uint8_t i : palette) {
|
||||
bmpOut.write(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Paeth predictor function per PNG spec
|
||||
static inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) {
|
||||
int p = static_cast<int>(a) + b - c;
|
||||
int pa = p > a ? p - a : a - p;
|
||||
int pb = p > b ? p - b : b - p;
|
||||
int pc = p > c ? p - c : c - p;
|
||||
if (pa <= pb && pa <= pc) return a;
|
||||
if (pb <= pc) return b;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Context for streaming PNG decompression
|
||||
struct PngDecodeContext {
|
||||
FsFile& file;
|
||||
|
||||
// PNG image properties
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint8_t bitDepth;
|
||||
uint8_t colorType;
|
||||
uint8_t bytesPerPixel; // after expanding sub-byte depths
|
||||
uint32_t rawRowBytes; // bytes per raw row (without filter byte)
|
||||
|
||||
// Scanline buffers
|
||||
uint8_t* currentRow; // current defiltered scanline
|
||||
uint8_t* previousRow; // previous defiltered scanline
|
||||
|
||||
// zlib decompression state
|
||||
mz_stream zstream;
|
||||
bool zstreamInitialized;
|
||||
|
||||
// Chunk reading state
|
||||
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
|
||||
bool idatFinished; // no more IDAT chunks
|
||||
|
||||
// File read buffer for feeding zlib
|
||||
uint8_t readBuf[2048];
|
||||
|
||||
// Palette for indexed color (type 3)
|
||||
uint8_t palette[256 * 3];
|
||||
int paletteSize;
|
||||
};
|
||||
|
||||
// Read the next IDAT chunk header, skipping non-IDAT chunks
|
||||
// Returns true if an IDAT chunk was found
|
||||
static bool findNextIdatChunk(PngDecodeContext& ctx) {
|
||||
while (true) {
|
||||
uint32_t chunkLen;
|
||||
if (!readBE32(ctx.file, chunkLen)) return false;
|
||||
|
||||
uint8_t chunkType[4];
|
||||
if (ctx.file.read(chunkType, 4) != 4) return false;
|
||||
|
||||
if (memcmp(chunkType, "IDAT", 4) == 0) {
|
||||
ctx.chunkBytesRemaining = chunkLen;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip this chunk's data + 4-byte CRC
|
||||
// Use seek to skip efficiently
|
||||
if (!ctx.file.seekCur(chunkLen + 4)) return false;
|
||||
|
||||
// If we hit IEND, there are no more chunks
|
||||
if (memcmp(chunkType, "IEND", 4) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed compressed data to zlib from IDAT chunks
|
||||
// Returns number of bytes made available in zstream, or -1 on error
|
||||
static int feedZlibInput(PngDecodeContext& ctx) {
|
||||
if (ctx.idatFinished) return 0;
|
||||
|
||||
// If current IDAT chunk is exhausted, skip its CRC and find next
|
||||
while (ctx.chunkBytesRemaining == 0) {
|
||||
// Skip 4-byte CRC of previous IDAT
|
||||
if (!ctx.file.seekCur(4)) return -1;
|
||||
|
||||
if (!findNextIdatChunk(ctx)) {
|
||||
ctx.idatFinished = true;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Read from current IDAT chunk
|
||||
size_t toRead = sizeof(ctx.readBuf);
|
||||
if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining;
|
||||
|
||||
int bytesRead = ctx.file.read(ctx.readBuf, toRead);
|
||||
if (bytesRead <= 0) return -1;
|
||||
|
||||
ctx.chunkBytesRemaining -= bytesRead;
|
||||
ctx.zstream.next_in = ctx.readBuf;
|
||||
ctx.zstream.avail_in = bytesRead;
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
// Decompress exactly 'needed' bytes into 'dest'
|
||||
static bool decompressBytes(PngDecodeContext& ctx, uint8_t* dest, size_t needed) {
|
||||
ctx.zstream.next_out = dest;
|
||||
ctx.zstream.avail_out = needed;
|
||||
|
||||
while (ctx.zstream.avail_out > 0) {
|
||||
if (ctx.zstream.avail_in == 0) {
|
||||
int fed = feedZlibInput(ctx);
|
||||
if (fed < 0) return false;
|
||||
if (fed == 0) {
|
||||
// Try one more inflate to flush
|
||||
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
|
||||
if (ctx.zstream.avail_out == 0) break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
|
||||
if (ret != MZ_OK && ret != MZ_STREAM_END && ret != MZ_BUF_ERROR) {
|
||||
LOG_ERR("PNG", "zlib inflate error: %d", ret);
|
||||
return false;
|
||||
}
|
||||
if (ret == MZ_STREAM_END) break;
|
||||
}
|
||||
|
||||
return ctx.zstream.avail_out == 0;
|
||||
}
|
||||
|
||||
// Decode one scanline: decompress filter byte + raw bytes, then unfilter
|
||||
static bool decodeScanline(PngDecodeContext& ctx) {
|
||||
// Decompress filter byte
|
||||
uint8_t filterType;
|
||||
if (!decompressBytes(ctx, &filterType, 1)) return false;
|
||||
|
||||
// Decompress raw row data into currentRow
|
||||
if (!decompressBytes(ctx, ctx.currentRow, ctx.rawRowBytes)) return false;
|
||||
|
||||
// Apply reverse filter
|
||||
const int bpp = ctx.bytesPerPixel;
|
||||
|
||||
switch (filterType) {
|
||||
case PNG_FILTER_NONE:
|
||||
break;
|
||||
|
||||
case PNG_FILTER_SUB:
|
||||
for (uint32_t i = bpp; i < ctx.rawRowBytes; i++) {
|
||||
ctx.currentRow[i] += ctx.currentRow[i - bpp];
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_FILTER_UP:
|
||||
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||
ctx.currentRow[i] += ctx.previousRow[i];
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_FILTER_AVERAGE:
|
||||
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||
uint8_t b = ctx.previousRow[i];
|
||||
ctx.currentRow[i] += (a + b) / 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_FILTER_PAETH:
|
||||
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||
uint8_t b = ctx.previousRow[i];
|
||||
uint8_t c = (i >= static_cast<uint32_t>(bpp)) ? ctx.previousRow[i - bpp] : 0;
|
||||
ctx.currentRow[i] += paethPredictor(a, b, c);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_ERR("PNG", "Unknown filter type: %d", filterType);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Batch-convert an entire scanline to grayscale.
|
||||
// Branches once on colorType/bitDepth, then runs a tight loop for the whole row.
|
||||
static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) {
|
||||
const uint8_t* src = ctx.currentRow;
|
||||
const uint32_t w = ctx.width;
|
||||
|
||||
switch (ctx.colorType) {
|
||||
case PNG_COLOR_GRAYSCALE:
|
||||
if (ctx.bitDepth == 8) {
|
||||
memcpy(grayRow, src, w);
|
||||
} else if (ctx.bitDepth == 16) {
|
||||
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
|
||||
} else {
|
||||
const int ppb = 8 / ctx.bitDepth;
|
||||
const uint8_t mask = (1 << ctx.bitDepth) - 1;
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
|
||||
grayRow[x] = (src[x / ppb] >> shift & mask) * 255 / mask;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_COLOR_RGB:
|
||||
if (ctx.bitDepth == 8) {
|
||||
// Fast path: most common EPUB cover format
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
const uint8_t* p = src + x * 3;
|
||||
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
|
||||
}
|
||||
} else {
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
grayRow[x] = (src[x * 6] * 25 + src[x * 6 + 2] * 50 + src[x * 6 + 4] * 25) / 100;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_COLOR_PALETTE: {
|
||||
const int ppb = 8 / ctx.bitDepth;
|
||||
const uint8_t mask = (1 << ctx.bitDepth) - 1;
|
||||
const uint8_t* pal = ctx.palette;
|
||||
const int palSize = ctx.paletteSize;
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
|
||||
uint8_t idx = (src[x / ppb] >> shift) & mask;
|
||||
if (idx >= palSize) idx = 0;
|
||||
grayRow[x] = (pal[idx * 3] * 25 + pal[idx * 3 + 1] * 50 + pal[idx * 3 + 2] * 25) / 100;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PNG_COLOR_GRAYSCALE_ALPHA:
|
||||
if (ctx.bitDepth == 8) {
|
||||
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
|
||||
} else {
|
||||
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 4];
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_COLOR_RGBA:
|
||||
if (ctx.bitDepth == 8) {
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
const uint8_t* p = src + x * 4;
|
||||
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
|
||||
}
|
||||
} else {
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
grayRow[x] = (src[x * 8] * 25 + src[x * 8 + 2] * 50 + src[x * 8 + 4] * 25) / 100;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
memset(grayRow, 128, w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||
bool oneBit, bool crop) {
|
||||
LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||
|
||||
// Verify PNG signature
|
||||
uint8_t sig[8];
|
||||
if (pngFile.read(sig, 8) != 8 || memcmp(sig, PNG_SIGNATURE, 8) != 0) {
|
||||
LOG_ERR("PNG", "Invalid PNG signature");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read IHDR chunk
|
||||
uint32_t ihdrLen;
|
||||
if (!readBE32(pngFile, ihdrLen)) return false;
|
||||
|
||||
uint8_t ihdrType[4];
|
||||
if (pngFile.read(ihdrType, 4) != 4 || memcmp(ihdrType, "IHDR", 4) != 0) {
|
||||
LOG_ERR("PNG", "Missing IHDR chunk");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t width, height;
|
||||
if (!readBE32(pngFile, width) || !readBE32(pngFile, height)) return false;
|
||||
|
||||
uint8_t ihdrRest[5];
|
||||
if (pngFile.read(ihdrRest, 5) != 5) return false;
|
||||
|
||||
uint8_t bitDepth = ihdrRest[0];
|
||||
uint8_t colorType = ihdrRest[1];
|
||||
uint8_t compression = ihdrRest[2];
|
||||
uint8_t filter = ihdrRest[3];
|
||||
uint8_t interlace = ihdrRest[4];
|
||||
|
||||
// Skip IHDR CRC
|
||||
pngFile.seekCur(4);
|
||||
|
||||
LOG_DBG("PNG", "Image: %ux%u, depth=%u, color=%u, interlace=%u", width, height, bitDepth, colorType, interlace);
|
||||
|
||||
if (compression != 0 || filter != 0) {
|
||||
LOG_ERR("PNG", "Unsupported compression/filter method");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (interlace != 0) {
|
||||
LOG_ERR("PNG", "Interlaced PNGs not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safety limits
|
||||
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
||||
|
||||
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width == 0 || height == 0) {
|
||||
LOG_ERR("PNG", "Image too large or zero (%ux%u)", width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate bytes per pixel and raw row bytes
|
||||
uint8_t bytesPerPixel;
|
||||
uint32_t rawRowBytes;
|
||||
|
||||
switch (colorType) {
|
||||
case PNG_COLOR_GRAYSCALE:
|
||||
if (bitDepth == 16) {
|
||||
bytesPerPixel = 2;
|
||||
rawRowBytes = width * 2;
|
||||
} else if (bitDepth == 8) {
|
||||
bytesPerPixel = 1;
|
||||
rawRowBytes = width;
|
||||
} else {
|
||||
// Sub-byte: 1, 2, or 4 bits
|
||||
bytesPerPixel = 1;
|
||||
rawRowBytes = (width * bitDepth + 7) / 8;
|
||||
}
|
||||
break;
|
||||
case PNG_COLOR_RGB:
|
||||
bytesPerPixel = (bitDepth == 16) ? 6 : 3;
|
||||
rawRowBytes = width * bytesPerPixel;
|
||||
break;
|
||||
case PNG_COLOR_PALETTE:
|
||||
bytesPerPixel = 1;
|
||||
rawRowBytes = (width * bitDepth + 7) / 8;
|
||||
break;
|
||||
case PNG_COLOR_GRAYSCALE_ALPHA:
|
||||
bytesPerPixel = (bitDepth == 16) ? 4 : 2;
|
||||
rawRowBytes = width * bytesPerPixel;
|
||||
break;
|
||||
case PNG_COLOR_RGBA:
|
||||
bytesPerPixel = (bitDepth == 16) ? 8 : 4;
|
||||
rawRowBytes = width * bytesPerPixel;
|
||||
break;
|
||||
default:
|
||||
LOG_ERR("PNG", "Unsupported color type: %d", colorType);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate raw row bytes won't cause memory issues
|
||||
if (rawRowBytes > 16384) {
|
||||
LOG_ERR("PNG", "Row too large: %u bytes", rawRowBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize decode context
|
||||
PngDecodeContext ctx = {.file = pngFile,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.bitDepth = bitDepth,
|
||||
.colorType = colorType,
|
||||
.bytesPerPixel = bytesPerPixel,
|
||||
.rawRowBytes = rawRowBytes,
|
||||
.currentRow = nullptr,
|
||||
.previousRow = nullptr,
|
||||
.zstream = {},
|
||||
.zstreamInitialized = false,
|
||||
.chunkBytesRemaining = 0,
|
||||
.idatFinished = false,
|
||||
.readBuf = {},
|
||||
.palette = {},
|
||||
.paletteSize = 0};
|
||||
|
||||
// Allocate scanline buffers
|
||||
ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes));
|
||||
ctx.previousRow = static_cast<uint8_t*>(calloc(rawRowBytes, 1));
|
||||
if (!ctx.currentRow || !ctx.previousRow) {
|
||||
LOG_ERR("PNG", "Failed to allocate scanline buffers (%u bytes each)", rawRowBytes);
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scan for PLTE chunk (palette) and first IDAT chunk
|
||||
// We need to read chunks until we find IDAT, collecting PLTE along the way
|
||||
bool foundIdat = false;
|
||||
while (!foundIdat) {
|
||||
uint32_t chunkLen;
|
||||
if (!readBE32(pngFile, chunkLen)) break;
|
||||
|
||||
uint8_t chunkType[4];
|
||||
if (pngFile.read(chunkType, 4) != 4) break;
|
||||
|
||||
if (memcmp(chunkType, "PLTE", 4) == 0) {
|
||||
int entries = chunkLen / 3;
|
||||
if (entries > 256) entries = 256;
|
||||
ctx.paletteSize = entries;
|
||||
size_t palBytes = entries * 3;
|
||||
pngFile.read(ctx.palette, palBytes);
|
||||
// Skip any remaining palette data
|
||||
if (chunkLen > palBytes) pngFile.seekCur(chunkLen - palBytes);
|
||||
pngFile.seekCur(4); // CRC
|
||||
} else if (memcmp(chunkType, "IDAT", 4) == 0) {
|
||||
ctx.chunkBytesRemaining = chunkLen;
|
||||
foundIdat = true;
|
||||
} else if (memcmp(chunkType, "IEND", 4) == 0) {
|
||||
break;
|
||||
} else {
|
||||
// Skip unknown chunk
|
||||
pngFile.seekCur(chunkLen + 4);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundIdat) {
|
||||
LOG_ERR("PNG", "No IDAT chunk found");
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize zlib decompression
|
||||
memset(&ctx.zstream, 0, sizeof(ctx.zstream));
|
||||
if (mz_inflateInit(&ctx.zstream) != MZ_OK) {
|
||||
LOG_ERR("PNG", "Failed to initialize zlib");
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
return false;
|
||||
}
|
||||
ctx.zstreamInitialized = true;
|
||||
|
||||
// Calculate output dimensions (same logic as JpegToBmpConverter)
|
||||
int outWidth = width;
|
||||
int outHeight = height;
|
||||
uint32_t scaleX_fp = 65536;
|
||||
uint32_t scaleY_fp = 65536;
|
||||
bool needsScaling = false;
|
||||
|
||||
if (targetWidth > 0 && targetHeight > 0 &&
|
||||
(static_cast<int>(width) > targetWidth || static_cast<int>(height) > targetHeight)) {
|
||||
const float scaleToFitWidth = static_cast<float>(targetWidth) / width;
|
||||
const float scaleToFitHeight = static_cast<float>(targetHeight) / height;
|
||||
float scale = 1.0;
|
||||
if (crop) {
|
||||
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
} else {
|
||||
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
}
|
||||
|
||||
outWidth = static_cast<int>(width * scale);
|
||||
outHeight = static_cast<int>(height * scale);
|
||||
if (outWidth < 1) outWidth = 1;
|
||||
if (outHeight < 1) outHeight = 1;
|
||||
|
||||
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth;
|
||||
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight;
|
||||
needsScaling = true;
|
||||
|
||||
LOG_DBG("PNG", "Pre-scaling %ux%u -> %dx%d (fit to %dx%d)", width, height, outWidth, outHeight, targetWidth,
|
||||
targetHeight);
|
||||
}
|
||||
|
||||
// Write BMP header
|
||||
int bytesPerRow;
|
||||
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
|
||||
bytesPerRow = (outWidth + 3) / 4 * 4;
|
||||
} else if (oneBit) {
|
||||
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
|
||||
bytesPerRow = (outWidth + 31) / 32 * 4;
|
||||
} else {
|
||||
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
|
||||
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
||||
}
|
||||
|
||||
// Allocate BMP row buffer
|
||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||
if (!rowBuffer) {
|
||||
LOG_ERR("PNG", "Failed to allocate row buffer");
|
||||
mz_inflateEnd(&ctx.zstream);
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create ditherers (same as JpegToBmpConverter)
|
||||
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
||||
FloydSteinbergDitherer* fsDitherer = nullptr;
|
||||
Atkinson1BitDitherer* atkinson1BitDitherer = nullptr;
|
||||
|
||||
if (oneBit) {
|
||||
atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth);
|
||||
} else if (!USE_8BIT_OUTPUT) {
|
||||
if (USE_ATKINSON) {
|
||||
atkinsonDitherer = new AtkinsonDitherer(outWidth);
|
||||
} else if (USE_FLOYD_STEINBERG) {
|
||||
fsDitherer = new FloydSteinbergDitherer(outWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Scaling accumulators
|
||||
uint32_t* rowAccum = nullptr;
|
||||
uint16_t* rowCount = nullptr;
|
||||
int currentOutY = 0;
|
||||
uint32_t nextOutY_srcStart = 0;
|
||||
|
||||
if (needsScaling) {
|
||||
rowAccum = new uint32_t[outWidth]();
|
||||
rowCount = new uint16_t[outWidth]();
|
||||
nextOutY_srcStart = scaleY_fp;
|
||||
}
|
||||
|
||||
// Allocate grayscale row buffer - batch-convert each scanline to avoid
|
||||
// per-pixel getPixelGray() switch overhead in the hot loops
|
||||
auto* grayRow = static_cast<uint8_t*>(malloc(width));
|
||||
if (!grayRow) {
|
||||
LOG_ERR("PNG", "Failed to allocate grayscale row buffer");
|
||||
delete[] rowAccum;
|
||||
delete[] rowCount;
|
||||
delete atkinsonDitherer;
|
||||
delete fsDitherer;
|
||||
delete atkinson1BitDitherer;
|
||||
free(rowBuffer);
|
||||
mz_inflateEnd(&ctx.zstream);
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
|
||||
// Process each scanline
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
// Decode one scanline
|
||||
if (!decodeScanline(ctx)) {
|
||||
LOG_ERR("PNG", "Failed to decode scanline %u", y);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Batch-convert entire scanline to grayscale (one branch, tight loop)
|
||||
convertScanlineToGray(ctx, grayRow);
|
||||
|
||||
if (!needsScaling) {
|
||||
// Direct output (no scaling)
|
||||
memset(rowBuffer, 0, bytesPerRow);
|
||||
|
||||
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
rowBuffer[x] = adjustPixel(grayRow[x]);
|
||||
}
|
||||
} else if (oneBit) {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t bit =
|
||||
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(grayRow[x], x) : quantize1bit(grayRow[x], x, y);
|
||||
const int byteIndex = x / 8;
|
||||
const int bitOffset = 7 - (x % 8);
|
||||
rowBuffer[byteIndex] |= (bit << bitOffset);
|
||||
}
|
||||
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
||||
} else {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = adjustPixel(grayRow[x]);
|
||||
uint8_t twoBit;
|
||||
if (atkinsonDitherer) {
|
||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||
} else if (fsDitherer) {
|
||||
twoBit = fsDitherer->processPixel(gray, x);
|
||||
} else {
|
||||
twoBit = quantize(gray, x, y);
|
||||
}
|
||||
const int byteIndex = (x * 2) / 8;
|
||||
const int bitOffset = 6 - ((x * 2) % 8);
|
||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||
}
|
||||
if (atkinsonDitherer)
|
||||
atkinsonDitherer->nextRow();
|
||||
else if (fsDitherer)
|
||||
fsDitherer->nextRow();
|
||||
}
|
||||
bmpOut.write(rowBuffer, bytesPerRow);
|
||||
} else {
|
||||
// Area-averaging scaling (same as JpegToBmpConverter)
|
||||
for (int outX = 0; outX < outWidth; outX++) {
|
||||
const int srcXStart = (static_cast<uint32_t>(outX) * scaleX_fp) >> 16;
|
||||
const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16;
|
||||
|
||||
int sum = 0;
|
||||
int count = 0;
|
||||
for (int srcX = srcXStart; srcX < srcXEnd && srcX < static_cast<int>(width); srcX++) {
|
||||
sum += grayRow[srcX];
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0 && srcXStart < static_cast<int>(width)) {
|
||||
sum = grayRow[srcXStart];
|
||||
count = 1;
|
||||
}
|
||||
|
||||
rowAccum[outX] += sum;
|
||||
rowCount[outX] += count;
|
||||
}
|
||||
|
||||
// Check if we've crossed into the next output row
|
||||
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
|
||||
|
||||
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
||||
memset(rowBuffer, 0, bytesPerRow);
|
||||
|
||||
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||
rowBuffer[x] = adjustPixel(gray);
|
||||
}
|
||||
} else if (oneBit) {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||
const uint8_t bit =
|
||||
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, currentOutY);
|
||||
const int byteIndex = x / 8;
|
||||
const int bitOffset = 7 - (x % 8);
|
||||
rowBuffer[byteIndex] |= (bit << bitOffset);
|
||||
}
|
||||
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
||||
} else {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
|
||||
uint8_t twoBit;
|
||||
if (atkinsonDitherer) {
|
||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||
} else if (fsDitherer) {
|
||||
twoBit = fsDitherer->processPixel(gray, x);
|
||||
} else {
|
||||
twoBit = quantize(gray, x, currentOutY);
|
||||
}
|
||||
const int byteIndex = (x * 2) / 8;
|
||||
const int bitOffset = 6 - ((x * 2) % 8);
|
||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||
}
|
||||
if (atkinsonDitherer)
|
||||
atkinsonDitherer->nextRow();
|
||||
else if (fsDitherer)
|
||||
fsDitherer->nextRow();
|
||||
}
|
||||
|
||||
bmpOut.write(rowBuffer, bytesPerRow);
|
||||
currentOutY++;
|
||||
|
||||
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
|
||||
memset(rowCount, 0, outWidth * sizeof(uint16_t));
|
||||
|
||||
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
|
||||
}
|
||||
}
|
||||
|
||||
// Swap current/previous row buffers
|
||||
uint8_t* temp = ctx.previousRow;
|
||||
ctx.previousRow = ctx.currentRow;
|
||||
ctx.currentRow = temp;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
free(grayRow);
|
||||
delete[] rowAccum;
|
||||
delete[] rowCount;
|
||||
delete atkinsonDitherer;
|
||||
delete fsDitherer;
|
||||
delete atkinson1BitDitherer;
|
||||
free(rowBuffer);
|
||||
mz_inflateEnd(&ctx.zstream);
|
||||
free(ctx.currentRow);
|
||||
free(ctx.previousRow);
|
||||
|
||||
if (success) {
|
||||
LOG_DBG("PNG", "Successfully converted PNG to BMP");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool PngToBmpConverter::pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop) {
|
||||
return pngFileToBmpStreamInternal(pngFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop);
|
||||
}
|
||||
|
||||
bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||
int targetMaxHeight) {
|
||||
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false);
|
||||
}
|
||||
|
||||
bool PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||
int targetMaxHeight) {
|
||||
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
|
||||
}
|
||||
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
class FsFile;
|
||||
class Print;
|
||||
|
||||
class PngToBmpConverter {
|
||||
static bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit,
|
||||
bool crop = true);
|
||||
|
||||
public:
|
||||
static bool pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop = true);
|
||||
static bool pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||
static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -98,13 +99,13 @@ std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.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 +121,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 +137,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 +157,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 +176,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
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); }
|
||||
54
lib/hal/HalStorage.h
Normal file
54
lib/hal/HalStorage.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class HalStorage {
|
||||
public:
|
||||
HalStorage();
|
||||
bool begin();
|
||||
bool ready() const;
|
||||
std::vector<String> listFiles(const char* path = "/", int maxFiles = 200);
|
||||
// Read the entire file at `path` into a String. Returns empty string on failure.
|
||||
String readFile(const char* path);
|
||||
// Low-memory helpers:
|
||||
// Stream the file contents to a `Print` (e.g. `Serial`, or any `Print`-derived object).
|
||||
// Returns true on success, false on failure.
|
||||
bool readFileToStream(const char* path, Print& out, size_t chunkSize = 256);
|
||||
// Read up to `bufferSize-1` bytes into `buffer`, null-terminating it. Returns bytes read.
|
||||
size_t readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes = 0);
|
||||
// Write a string to `path` on the SD card. Overwrites existing file.
|
||||
// Returns true on success.
|
||||
bool writeFile(const char* path, const String& content);
|
||||
// Ensure a directory exists, creating it if necessary. Returns true on success.
|
||||
bool ensureDirectoryExists(const char* path);
|
||||
|
||||
FsFile open(const char* path, const oflag_t oflag = O_RDONLY);
|
||||
bool mkdir(const char* path, const bool pFlag = true);
|
||||
bool exists(const char* path);
|
||||
bool remove(const char* path);
|
||||
bool rmdir(const char* path);
|
||||
|
||||
bool openFileForRead(const char* moduleName, const char* path, FsFile& file);
|
||||
bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file);
|
||||
bool openFileForRead(const char* moduleName, const String& path, FsFile& file);
|
||||
bool openFileForWrite(const char* moduleName, const char* path, FsFile& file);
|
||||
bool openFileForWrite(const char* moduleName, const std::string& path, FsFile& file);
|
||||
bool openFileForWrite(const char* moduleName, const String& path, FsFile& file);
|
||||
bool removeDir(const char* path);
|
||||
|
||||
static HalStorage& getInstance() { return instance; }
|
||||
|
||||
private:
|
||||
static HalStorage instance;
|
||||
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
#define Storage HalStorage::getInstance()
|
||||
|
||||
// Downstream code must use Storage instead of SdMan
|
||||
#ifdef SdMan
|
||||
#undef SdMan
|
||||
#endif
|
||||
@@ -22,14 +22,21 @@ build_flags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||
-DMINIZ_NO_STDIO=1
|
||||
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
|
||||
-DDISABLE_FS_H_WARNING=1
|
||||
# https://libexpat.github.io/doc/api/latest/#XML_GE
|
||||
-DXML_GE=0
|
||||
-DXML_CONTEXT_BYTES=1024
|
||||
-std=c++2a
|
||||
-std=gnu++2a
|
||||
# Enable UTF-8 long file names in SdFat
|
||||
-DUSE_UTF8_LONG_NAMES=1
|
||||
# Increase PNG scanline buffer to support up to 800px wide images
|
||||
# Default is (320*4+1)*2=2562, we need more for larger images
|
||||
-DPNG_MAX_BUFFERED_PIXELS=6402
|
||||
|
||||
build_unflags =
|
||||
-std=gnu++11
|
||||
|
||||
; Board configuration
|
||||
board_build.flash_mode = dio
|
||||
@@ -38,6 +45,7 @@ board_build.partitions = partitions.csv
|
||||
|
||||
extra_scripts =
|
||||
pre:scripts/build_html.py
|
||||
pre:scripts/gen_i18n.py
|
||||
|
||||
; Libraries
|
||||
lib_deps =
|
||||
@@ -47,6 +55,7 @@ lib_deps =
|
||||
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
||||
bblanchon/ArduinoJson @ 7.4.2
|
||||
ricmoo/QRCode @ 0.0.1
|
||||
bitbank2/PNGdec @ ^1.0.0
|
||||
links2004/WebSockets @ 2.7.3
|
||||
|
||||
[env:default]
|
||||
@@ -54,15 +63,31 @@ extends = base
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-dev\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
||||
|
||||
|
||||
[env:gh_release]
|
||||
extends = base
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=0 ; Set log level to error for release builds
|
||||
|
||||
[env:gh_release_rc]
|
||||
extends = base
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
||||
|
||||
[env:slim]
|
||||
extends = base
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-slim\"
|
||||
; serial output is disabled in slim builds to save space
|
||||
-UENABLE_SERIAL_LOG
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import gzip
|
||||
|
||||
SRC_DIR = "src"
|
||||
|
||||
@@ -40,12 +41,34 @@ for root, _, files in os.walk(SRC_DIR):
|
||||
|
||||
# minified = regex.sub("\g<1>", html_content)
|
||||
minified = minify_html(html_content)
|
||||
|
||||
# Compress with gzip (compresslevel 9 is maximum compression)
|
||||
# IMPORTANT: we don't use brotli because Firefox doesn't support brotli with insecured context (only supported on HTTPS)
|
||||
compressed = gzip.compress(minified.encode('utf-8'), compresslevel=9)
|
||||
|
||||
base_name = f"{os.path.splitext(file)[0]}Html"
|
||||
header_path = os.path.join(root, f"{base_name}.generated.h")
|
||||
|
||||
with open(header_path, "w", encoding="utf-8") as h:
|
||||
h.write(f"// THIS FILE IS AUTOGENERATED, DO NOT EDIT MANUALLY\n\n")
|
||||
h.write(f"#pragma once\n")
|
||||
h.write(f'constexpr char {base_name}[] PROGMEM = R"rawliteral({minified})rawliteral";\n')
|
||||
h.write(f"#include <cstddef>\n\n")
|
||||
|
||||
# Write the compressed data as a byte array
|
||||
h.write(f"constexpr char {base_name}[] PROGMEM = {{\n")
|
||||
|
||||
# Write bytes in rows of 16
|
||||
for i in range(0, len(compressed), 16):
|
||||
chunk = compressed[i:i+16]
|
||||
hex_values = ', '.join(f'0x{b:02x}' for b in chunk)
|
||||
h.write(f" {hex_values},\n")
|
||||
|
||||
h.write(f"}};\n\n")
|
||||
h.write(f"constexpr size_t {base_name}CompressedSize = {len(compressed)};\n")
|
||||
h.write(f"constexpr size_t {base_name}OriginalSize = {len(minified)};\n")
|
||||
|
||||
print(f"Generated: {header_path}")
|
||||
print(f" Original: {len(html_content)} bytes")
|
||||
print(f" Minified: {len(minified)} bytes ({100*len(minified)/len(html_content):.1f}%)")
|
||||
print(f" Compressed: {len(compressed)} bytes ({100*len(compressed)/len(html_content):.1f}%)")
|
||||
|
||||
|
||||
@@ -1,32 +1,73 @@
|
||||
import sys
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ESP32 Serial Monitor with Memory Graph
|
||||
|
||||
This script provides a comprehensive real-time serial monitor for ESP32 devices with
|
||||
integrated memory usage graphing capabilities. It reads serial output, parses memory
|
||||
information, and displays it in both console and graphical form.
|
||||
|
||||
Features:
|
||||
- Real-time serial output monitoring with color-coded log levels
|
||||
- Interactive memory usage graphing with matplotlib
|
||||
- Command input interface for sending commands to the ESP32 device
|
||||
- Screenshot capture and processing (1-bit black/white format)
|
||||
- Graceful shutdown handling with Ctrl-C signal processing
|
||||
- Configurable filtering and suppression of log messages
|
||||
- Thread-safe operation with coordinated shutdown events
|
||||
|
||||
Usage:
|
||||
python debugging_monitor.py [port] [options]
|
||||
|
||||
The script will open a matplotlib window showing memory usage over time and provide
|
||||
an interactive command prompt for sending commands to the device. Press Ctrl-C or
|
||||
close the graph window to exit gracefully.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import platform
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# Try to import potentially missing packages
|
||||
PACKAGE_MAPPING: dict[str, str] = {
|
||||
"serial": "pyserial",
|
||||
"colorama": "colorama",
|
||||
"matplotlib": "matplotlib",
|
||||
"PIL": "Pillow",
|
||||
}
|
||||
|
||||
try:
|
||||
import serial
|
||||
from colorama import init, Fore, Style
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import serial
|
||||
from colorama import Fore, Style, init
|
||||
from matplotlib import animation
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
Image = None
|
||||
except ImportError as e:
|
||||
missing_package = e.name
|
||||
ERROR_MSG = str(e).lower()
|
||||
missing_packages = [pkg for mod, pkg in PACKAGE_MAPPING.items() if mod in ERROR_MSG]
|
||||
|
||||
if not missing_packages:
|
||||
# Fallback if mapping doesn't cover
|
||||
missing_packages = ["pyserial", "colorama", "matplotlib"]
|
||||
|
||||
print("\n" + "!" * 50)
|
||||
print(f" Error: The required package '{missing_package}' is not installed.")
|
||||
print(f" Error: Required package(s) not installed: {', '.join(missing_packages)}")
|
||||
print("!" * 50)
|
||||
|
||||
print(f"\nTo fix this, please run the following command in your terminal:\n")
|
||||
|
||||
install_cmd = "pip install "
|
||||
packages = []
|
||||
if 'serial' in str(e): packages.append("pyserial")
|
||||
if 'colorama' in str(e): packages.append("colorama")
|
||||
if 'matplotlib' in str(e): packages.append("matplotlib")
|
||||
|
||||
print(f" {install_cmd}{' '.join(packages)}")
|
||||
print("\nTo fix this, please run the following command in your terminal:\n")
|
||||
INSTALL_CMD = "pip install " if sys.platform.startswith("win") else "pip3 install "
|
||||
print(f" {INSTALL_CMD}{' '.join(missing_packages)}")
|
||||
|
||||
print("\nExiting...")
|
||||
sys.exit(1)
|
||||
@@ -34,50 +75,104 @@ except ImportError as e:
|
||||
# --- Global Variables for Data Sharing ---
|
||||
# Store last 50 data points
|
||||
MAX_POINTS = 50
|
||||
time_data = deque(maxlen=MAX_POINTS)
|
||||
free_mem_data = deque(maxlen=MAX_POINTS)
|
||||
total_mem_data = deque(maxlen=MAX_POINTS)
|
||||
data_lock = threading.Lock() # Prevent reading while writing
|
||||
time_data: deque[str] = deque(maxlen=MAX_POINTS)
|
||||
free_mem_data: deque[float] = deque(maxlen=MAX_POINTS)
|
||||
total_mem_data: deque[float] = deque(maxlen=MAX_POINTS)
|
||||
data_lock: threading.Lock = threading.Lock() # Prevent reading while writing
|
||||
|
||||
# Global shutdown flag
|
||||
shutdown_event = threading.Event()
|
||||
|
||||
# Initialize colors
|
||||
init(autoreset=True)
|
||||
|
||||
def get_color_for_line(line):
|
||||
# Color mapping for log lines
|
||||
COLOR_KEYWORDS: dict[str, list[str]] = {
|
||||
Fore.RED: ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"],
|
||||
Fore.CYAN: ["[MEM]", "FREE:"],
|
||||
Fore.MAGENTA: [
|
||||
"[GFX]",
|
||||
"[ERS]",
|
||||
"DISPLAY",
|
||||
"RAM WRITE",
|
||||
"RAM COMPLETE",
|
||||
"REFRESH",
|
||||
"POWERING ON",
|
||||
"FRAME BUFFER",
|
||||
"LUT",
|
||||
],
|
||||
Fore.GREEN: [
|
||||
"[EBP]",
|
||||
"[BMC]",
|
||||
"[ZIP]",
|
||||
"[PARSER]",
|
||||
"[EHP]",
|
||||
"LOADING EPUB",
|
||||
"CACHE",
|
||||
"DECOMPRESSED",
|
||||
"PARSING",
|
||||
],
|
||||
Fore.YELLOW: ["[ACT]", "ENTERING ACTIVITY", "EXITING ACTIVITY"],
|
||||
Fore.BLUE: ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"],
|
||||
Fore.LIGHTYELLOW_EX: [
|
||||
"[CPS]",
|
||||
"SETTINGS",
|
||||
"[CLEAR_CACHE]",
|
||||
"[CHAP]",
|
||||
"[OPDS]",
|
||||
"[COF]",
|
||||
],
|
||||
Fore.LIGHTBLACK_EX: [
|
||||
"ESP-ROM",
|
||||
"BUILD:",
|
||||
"RST:",
|
||||
"BOOT:",
|
||||
"SPIWP:",
|
||||
"MODE:",
|
||||
"LOAD:",
|
||||
"ENTRY",
|
||||
"[SD]",
|
||||
"STARTING CROSSPOINT",
|
||||
"VERSION",
|
||||
],
|
||||
Fore.LIGHTCYAN_EX: ["[RBS]"],
|
||||
Fore.LIGHTMAGENTA_EX: [
|
||||
"[KRS]",
|
||||
"EINKDISPLAY:",
|
||||
"STATIC FRAME",
|
||||
"INITIALIZING",
|
||||
"SPI INITIALIZED",
|
||||
"GPIO PINS",
|
||||
"RESETTING",
|
||||
"SSD1677",
|
||||
"E-INK",
|
||||
],
|
||||
Fore.LIGHTGREEN_EX: ["[FNS]", "FOOTNOTE"],
|
||||
}
|
||||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Handle SIGINT (Ctrl-C) by setting the shutdown event."""
|
||||
# frame parameter is required by signal handler signature but not used
|
||||
del frame # Explicitly mark as unused to satisfy linters
|
||||
print(f"\n{Fore.YELLOW}Received signal {signum}. Shutting down...{Style.RESET_ALL}")
|
||||
shutdown_event.set()
|
||||
plt.close("all")
|
||||
|
||||
|
||||
# pylint: disable=R0912
|
||||
def get_color_for_line(line: str) -> str:
|
||||
"""
|
||||
Classify log lines by type and assign appropriate colors.
|
||||
"""
|
||||
line_upper = line.upper()
|
||||
|
||||
if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]):
|
||||
return Fore.RED
|
||||
if "[MEM]" in line_upper or "FREE:" in line_upper:
|
||||
return Fore.CYAN
|
||||
if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]):
|
||||
return Fore.MAGENTA
|
||||
if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]):
|
||||
return Fore.GREEN
|
||||
if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper:
|
||||
return Fore.YELLOW
|
||||
if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]):
|
||||
return Fore.BLUE
|
||||
if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]):
|
||||
return Fore.LIGHTYELLOW_EX
|
||||
if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]):
|
||||
return Fore.LIGHTBLACK_EX
|
||||
if "[RBS]" in line_upper:
|
||||
return Fore.LIGHTCYAN_EX
|
||||
if "[KRS]" in line_upper:
|
||||
return Fore.LIGHTMAGENTA_EX
|
||||
if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]):
|
||||
return Fore.LIGHTMAGENTA_EX
|
||||
if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]):
|
||||
return Fore.LIGHTGREEN_EX
|
||||
if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]):
|
||||
return Fore.LIGHTYELLOW_EX
|
||||
|
||||
for color, keywords in COLOR_KEYWORDS.items():
|
||||
if any(keyword in line_upper for keyword in keywords):
|
||||
return color
|
||||
return Fore.WHITE
|
||||
|
||||
def parse_memory_line(line):
|
||||
|
||||
def parse_memory_line(line: str) -> tuple[int | None, int | None]:
|
||||
"""
|
||||
Extracts Free and Total bytes from the specific log line.
|
||||
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
||||
@@ -93,122 +188,321 @@ def parse_memory_line(line):
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
def serial_worker(port, baud):
|
||||
|
||||
def serial_worker(ser, kwargs: dict[str, str]) -> None:
|
||||
"""
|
||||
Runs in a background thread. Handles reading serial, printing to console,
|
||||
and updating the data lists.
|
||||
Runs in a background thread. Handles reading serial data, printing to console,
|
||||
updating memory usage data for graphing, and processing screenshot data.
|
||||
Monitors the global shutdown event for graceful termination.
|
||||
"""
|
||||
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}--- Opening serial port ---{Style.RESET_ALL}")
|
||||
filter_keyword = kwargs.get("filter", "").lower()
|
||||
suppress = kwargs.get("suppress", "").lower()
|
||||
if filter_keyword and suppress and filter_keyword == suppress:
|
||||
print(
|
||||
f"{Fore.YELLOW}Warning: Filter and Suppress keywords are the same. "
|
||||
f"This may result in no output.{Style.RESET_ALL}"
|
||||
)
|
||||
if filter_keyword:
|
||||
print(
|
||||
f"{Fore.YELLOW}Filtering lines to only show those containing: "
|
||||
f"'{filter_keyword}'{Style.RESET_ALL}"
|
||||
)
|
||||
if suppress:
|
||||
print(
|
||||
f"{Fore.YELLOW}Suppressing lines containing: '{suppress}'{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
expecting_screenshot = False
|
||||
screenshot_size = 0
|
||||
screenshot_data = b""
|
||||
|
||||
try:
|
||||
ser = serial.Serial(port, baud, timeout=0.1)
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
except serial.SerialException as e:
|
||||
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
raw_data = ser.readline().decode('utf-8', errors='replace')
|
||||
|
||||
if not raw_data:
|
||||
while not shutdown_event.is_set():
|
||||
if expecting_screenshot:
|
||||
data = ser.read(screenshot_size - len(screenshot_data))
|
||||
if not data:
|
||||
continue
|
||||
screenshot_data += data
|
||||
if len(screenshot_data) == screenshot_size:
|
||||
if Image:
|
||||
img = Image.frombytes("1", (800, 480), screenshot_data)
|
||||
# We need to rotate the image because the raw data is in landscape mode
|
||||
img = img.transpose(Image.ROTATE_270)
|
||||
img.save("screenshot.bmp")
|
||||
print(
|
||||
f"{Fore.GREEN}Screenshot saved to screenshot.bmp{Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
with open("screenshot.raw", "wb") as f:
|
||||
f.write(screenshot_data)
|
||||
print(
|
||||
f"{Fore.GREEN}Screenshot saved to screenshot.raw (PIL not available){Style.RESET_ALL}"
|
||||
)
|
||||
expecting_screenshot = False
|
||||
screenshot_data = b""
|
||||
else:
|
||||
try:
|
||||
raw_data = ser.readline().decode("utf-8", errors="replace")
|
||||
|
||||
clean_line = raw_data.strip()
|
||||
if not clean_line:
|
||||
continue
|
||||
if not raw_data:
|
||||
continue
|
||||
|
||||
# Add PC timestamp
|
||||
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||
clean_line = raw_data.strip()
|
||||
if not clean_line:
|
||||
continue
|
||||
|
||||
# Check for Memory Line
|
||||
if "[MEM]" in formatted_line:
|
||||
free_val, total_val = parse_memory_line(formatted_line)
|
||||
if free_val is not None:
|
||||
with data_lock:
|
||||
time_data.append(pc_time)
|
||||
free_mem_data.append(free_val / 1024) # Convert to KB
|
||||
total_mem_data.append(total_val / 1024) # Convert to KB
|
||||
if clean_line.startswith("SCREENSHOT_START:"):
|
||||
screenshot_size = int(clean_line.split(":")[1])
|
||||
expecting_screenshot = True
|
||||
continue
|
||||
elif clean_line == "SCREENSHOT_END":
|
||||
continue # ignore
|
||||
|
||||
# Print to console
|
||||
line_color = get_color_for_line(formatted_line)
|
||||
print(f"{line_color}{formatted_line}")
|
||||
# Add PC timestamp
|
||||
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||
|
||||
except OSError:
|
||||
print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}")
|
||||
break
|
||||
except Exception as e:
|
||||
# Check for Memory Line
|
||||
if "[MEM]" in formatted_line:
|
||||
free_val, total_val = parse_memory_line(formatted_line)
|
||||
if free_val is not None and total_val is not None:
|
||||
with data_lock:
|
||||
time_data.append(pc_time)
|
||||
free_mem_data.append(free_val / 1024) # Convert to KB
|
||||
total_mem_data.append(total_val / 1024) # Convert to KB
|
||||
# Apply filters
|
||||
if filter_keyword and filter_keyword not in formatted_line.lower():
|
||||
continue
|
||||
if suppress and suppress in formatted_line.lower():
|
||||
continue
|
||||
# Print to console
|
||||
line_color = get_color_for_line(formatted_line)
|
||||
print(f"{line_color}{formatted_line}")
|
||||
|
||||
except (OSError, UnicodeDecodeError):
|
||||
print(
|
||||
f"{Fore.RED}Device disconnected or data error.{Style.RESET_ALL}"
|
||||
)
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
# If thread is killed violently (e.g. main exit), silence errors
|
||||
pass
|
||||
finally:
|
||||
if 'ser' in locals() and ser.is_open:
|
||||
ser.close()
|
||||
pass # ser closed in main
|
||||
|
||||
def update_graph(frame):
|
||||
|
||||
def input_worker(ser) -> None:
|
||||
"""
|
||||
Called by Matplotlib animation to redraw the chart.
|
||||
Runs in a background thread. Handles user input to send commands to the ESP32 device.
|
||||
Monitors the global shutdown event for graceful termination on Ctrl-C.
|
||||
"""
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
cmd = input("Command: ")
|
||||
ser.write(f"CMD:{cmd}\n".encode())
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
|
||||
|
||||
def update_graph(frame) -> list: # pylint: disable=unused-argument
|
||||
"""
|
||||
Called by Matplotlib animation to redraw the memory usage chart.
|
||||
Monitors the global shutdown event and closes the plot when shutdown is requested.
|
||||
"""
|
||||
if shutdown_event.is_set():
|
||||
plt.close("all")
|
||||
return []
|
||||
|
||||
with data_lock:
|
||||
if not time_data:
|
||||
return
|
||||
return []
|
||||
|
||||
# Convert deques to lists for plotting
|
||||
x = list(time_data)
|
||||
y_free = list(free_mem_data)
|
||||
y_total = list(total_mem_data)
|
||||
|
||||
plt.cla() # Clear axis
|
||||
plt.cla() # Clear axis
|
||||
|
||||
# Plot Total RAM
|
||||
plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--')
|
||||
plt.plot(x, y_total, label="Total RAM (KB)", color="red", linestyle="--")
|
||||
|
||||
# Plot Free RAM
|
||||
plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o')
|
||||
plt.plot(x, y_free, label="Free RAM (KB)", color="green", marker="o")
|
||||
|
||||
# Fill area under Free RAM
|
||||
plt.fill_between(x, y_free, color='green', alpha=0.1)
|
||||
plt.fill_between(x, y_free, color="green", alpha=0.1)
|
||||
|
||||
plt.title("ESP32 Memory Monitor")
|
||||
plt.ylabel("Memory (KB)")
|
||||
plt.xlabel("Time")
|
||||
plt.legend(loc='upper left')
|
||||
plt.grid(True, linestyle=':', alpha=0.6)
|
||||
plt.legend(loc="upper left")
|
||||
plt.grid(True, linestyle=":", alpha=0.6)
|
||||
|
||||
# Rotate date labels
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.xticks(rotation=45, ha="right")
|
||||
plt.tight_layout()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
|
||||
parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="Baud rate")
|
||||
return []
|
||||
|
||||
|
||||
def get_auto_detected_port() -> list[str]:
|
||||
"""
|
||||
Attempts to auto-detect the serial port for the ESP32 device.
|
||||
Returns a list of all detected ports.
|
||||
If no suitable port is found, the list will be empty.
|
||||
Darwin/Linux logic by jonasdiemer
|
||||
"""
|
||||
port_list = []
|
||||
system = platform.system()
|
||||
# Code for darwin (macOS), linux, and windows
|
||||
if system in ("Darwin", "Linux"):
|
||||
pattern = "/dev/tty.usbmodem*" if system == "Darwin" else "/dev/ttyACM*"
|
||||
port_list = sorted(glob.glob(pattern))
|
||||
elif system == "Windows":
|
||||
from serial.tools import list_ports
|
||||
|
||||
# Be careful with this pattern list - it should be specific
|
||||
# enough to avoid picking up unrelated devices, but broad enough
|
||||
# to catch all common USB-serial adapters used with ESP32
|
||||
# Caveat: localized versions of Windows may have different descriptions,
|
||||
# so we also check for specific VID:PID (but that may not cover all clones)
|
||||
pattern_list = ["CP210x", "CH340", "USB Serial"]
|
||||
found_ports = list_ports.comports()
|
||||
port_list = [
|
||||
port.device
|
||||
for port in found_ports
|
||||
if any(pat in port.description for pat in pattern_list)
|
||||
or port.hwid.startswith(
|
||||
"USB VID:PID=303A:1001"
|
||||
) # Add specific VID:PID for XTEINK X4
|
||||
]
|
||||
|
||||
return port_list
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Main entry point for the ESP32 monitor application.
|
||||
|
||||
Sets up argument parsing, initializes serial communication, starts background threads
|
||||
for serial monitoring and command input, and launches the memory usage graph.
|
||||
Implements graceful shutdown handling with signal processing for clean termination.
|
||||
|
||||
Features:
|
||||
- Serial port monitoring with color-coded output
|
||||
- Real-time memory usage graphing
|
||||
- Interactive command interface
|
||||
- Screenshot capture capability
|
||||
- Graceful shutdown on Ctrl-C or window close
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="ESP32 Serial Monitor with Memory Graph - Real-time monitoring, graphing, and command interface"
|
||||
)
|
||||
default_baudrate = 115200
|
||||
parser.add_argument(
|
||||
"port",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Serial port (leave empty for autodetection)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--baud",
|
||||
type=int,
|
||||
default=default_baudrate,
|
||||
help=f"Baud rate (default: {default_baudrate})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filter",
|
||||
type=str,
|
||||
default="",
|
||||
help="Only display lines containing this keyword (case-insensitive)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--suppress",
|
||||
type=str,
|
||||
default="",
|
||||
help="Suppress lines containing this keyword (case-insensitive)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
port = args.port
|
||||
if port is None:
|
||||
port_list = get_auto_detected_port()
|
||||
if len(port_list) == 1:
|
||||
port = port_list[0]
|
||||
print(f"{Fore.CYAN}Auto-detected serial port: {port}{Style.RESET_ALL}")
|
||||
elif len(port_list) > 1:
|
||||
print(f"{Fore.YELLOW}Multiple serial ports found:{Style.RESET_ALL}")
|
||||
for p in port_list:
|
||||
print(f" - {p}")
|
||||
print(
|
||||
f"{Fore.YELLOW}Please specify the desired port as a command-line argument.{Style.RESET_ALL}"
|
||||
)
|
||||
if port is None:
|
||||
print(f"{Fore.RED}Error: No suitable serial port found.{Style.RESET_ALL}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
ser = serial.Serial(port, args.baud, timeout=0.1)
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
except serial.SerialException as e:
|
||||
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
# Set up signal handler for graceful shutdown
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# 1. Start the Serial Reader in a separate thread
|
||||
# Daemon=True means this thread dies when the main program closes
|
||||
t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True)
|
||||
myargs = vars(args) # Convert Namespace to dict for easier passing
|
||||
t = threading.Thread(target=serial_worker, args=(ser, myargs), daemon=True)
|
||||
t.start()
|
||||
|
||||
# Start input thread
|
||||
input_thread = threading.Thread(target=input_worker, args=(ser,), daemon=True)
|
||||
input_thread.start()
|
||||
|
||||
# 2. Set up the Graph (Main Thread)
|
||||
try:
|
||||
plt.style.use('light_background')
|
||||
except:
|
||||
import matplotlib.style as mplstyle # pylint: disable=import-outside-toplevel
|
||||
|
||||
default_styles = (
|
||||
"light_background",
|
||||
"ggplot",
|
||||
"seaborn",
|
||||
"dark_background",
|
||||
)
|
||||
styles = list(mplstyle.available)
|
||||
for default_style in default_styles:
|
||||
if default_style in styles:
|
||||
print(
|
||||
f"\n{Fore.CYAN}--- Using Matplotlib style: {default_style} ---{Style.RESET_ALL}"
|
||||
)
|
||||
mplstyle.use(default_style)
|
||||
break
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
|
||||
fig = plt.figure(figsize=(10, 6))
|
||||
|
||||
# Update graph every 1000ms
|
||||
ani = animation.FuncAnimation(fig, update_graph, interval=1000)
|
||||
_ = animation.FuncAnimation(
|
||||
fig, update_graph, interval=1000, cache_frame_data=False
|
||||
)
|
||||
|
||||
try:
|
||||
print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}")
|
||||
print(
|
||||
f"{Fore.YELLOW}Starting Graph Window... (Close window or press Ctrl-C to exit){Style.RESET_ALL}"
|
||||
)
|
||||
plt.show()
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
||||
plt.close('all') # Force close any lingering plot windows
|
||||
finally:
|
||||
shutdown_event.set() # Ensure all threads know to stop
|
||||
plt.close("all") # Force close any lingering plot windows
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
620
scripts/gen_i18n.py
Executable file
620
scripts/gen_i18n.py
Executable file
@@ -0,0 +1,620 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate I18n C++ files from per-language YAML translations.
|
||||
|
||||
Reads YAML files from a translations directory (one file per language) and generates:
|
||||
- I18nKeys.h: Language enum, StrId enum, helper functions
|
||||
- I18nStrings.h: String array declarations
|
||||
- I18nStrings.cpp: String array definitions with all translations
|
||||
|
||||
Each YAML file must contain:
|
||||
_language_name: "Native Name" (e.g. "Español")
|
||||
_language_code: "ENUM_NAME" (e.g. "SPANISH")
|
||||
STR_KEY: "translation text"
|
||||
|
||||
The English file is the reference. Missing keys in other languages are
|
||||
automatically filled from English, with a warning.
|
||||
|
||||
Usage:
|
||||
python gen_i18n.py <translations_dir> <output_dir>
|
||||
|
||||
Example:
|
||||
python gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# YAML file reading (simple key: "value" format, no PyYAML dependency)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _unescape_yaml_value(raw: str, filepath: str = "", line_num: int = 0) -> str:
|
||||
"""
|
||||
Process escape sequences in a YAML value string.
|
||||
|
||||
Recognized escapes: \\\\ → \\ \\" → " \\n → newline
|
||||
"""
|
||||
result: List[str] = []
|
||||
i = 0
|
||||
while i < len(raw):
|
||||
if raw[i] == "\\" and i + 1 < len(raw):
|
||||
nxt = raw[i + 1]
|
||||
if nxt == "\\":
|
||||
result.append("\\")
|
||||
elif nxt == '"':
|
||||
result.append('"')
|
||||
elif nxt == "n":
|
||||
result.append("\n")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{filepath}:{line_num}: unknown escape '\\{nxt}'"
|
||||
)
|
||||
i += 2
|
||||
else:
|
||||
result.append(raw[i])
|
||||
i += 1
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def parse_yaml_file(filepath: str) -> Dict[str, str]:
|
||||
"""
|
||||
Parse a simple YAML file of the form:
|
||||
key: "value"
|
||||
|
||||
Only supports flat key-value pairs with quoted string values.
|
||||
Aborts on formatting errors.
|
||||
"""
|
||||
result = {}
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
for line_num, raw_line in enumerate(f, start=1):
|
||||
line = raw_line.rstrip("\n\r")
|
||||
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
match = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*"(.*)"$', line)
|
||||
if not match:
|
||||
raise ValueError(
|
||||
f"{filepath}:{line_num}: bad format: {line!r}\n"
|
||||
f' Expected: KEY: "value"'
|
||||
)
|
||||
|
||||
key = match.group(1)
|
||||
raw_value = match.group(2)
|
||||
|
||||
# Un-escape: process character by character to handle
|
||||
# \\, \", and \n sequences correctly
|
||||
value = _unescape_yaml_value(raw_value, filepath, line_num)
|
||||
|
||||
if key in result:
|
||||
raise ValueError(f"{filepath}:{line_num}: duplicate key '{key}'")
|
||||
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load all languages from a directory of YAML files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_translations(
|
||||
translations_dir: str,
|
||||
) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]]]:
|
||||
"""
|
||||
Read every YAML file in *translations_dir* and return:
|
||||
language_codes e.g. ["ENGLISH", "SPANISH", ...]
|
||||
language_names e.g. ["English", "Español", ...]
|
||||
string_keys ordered list of STR_* keys (from English)
|
||||
translations {key: [translation_per_language]}
|
||||
|
||||
English is always first;
|
||||
"""
|
||||
yaml_dir = Path(translations_dir)
|
||||
if not yaml_dir.is_dir():
|
||||
raise FileNotFoundError(f"Translations directory not found: {translations_dir}")
|
||||
|
||||
yaml_files = sorted(yaml_dir.glob("*.yaml"))
|
||||
if not yaml_files:
|
||||
raise FileNotFoundError(f"No .yaml files found in {translations_dir}")
|
||||
|
||||
# Parse every file
|
||||
parsed: Dict[str, Dict[str, str]] = {}
|
||||
for yf in yaml_files:
|
||||
parsed[yf.name] = parse_yaml_file(str(yf))
|
||||
|
||||
# Identify the English file (must exist)
|
||||
english_file = None
|
||||
for name, data in parsed.items():
|
||||
if data.get("_language_code", "").upper() == "ENGLISH":
|
||||
english_file = name
|
||||
break
|
||||
|
||||
if english_file is None:
|
||||
raise ValueError("No YAML file with _language_code: ENGLISH found")
|
||||
|
||||
# Order: English first, then by _order metadata (falls back to filename)
|
||||
def sort_key(fname: str) -> Tuple[int, int, str]:
|
||||
"""English always first (0), then by _order, then by filename."""
|
||||
if fname == english_file:
|
||||
return (0, 0, fname)
|
||||
order = parsed[fname].get("_order", "999")
|
||||
try:
|
||||
order_int = int(order)
|
||||
except ValueError:
|
||||
order_int = 999
|
||||
return (1, order_int, fname)
|
||||
|
||||
ordered_files = sorted(parsed, key=sort_key)
|
||||
|
||||
# Extract metadata
|
||||
language_codes: List[str] = []
|
||||
language_names: List[str] = []
|
||||
for fname in ordered_files:
|
||||
data = parsed[fname]
|
||||
code = data.get("_language_code")
|
||||
name = data.get("_language_name")
|
||||
if not code or not name:
|
||||
raise ValueError(f"{fname}: missing _language_code or _language_name")
|
||||
language_codes.append(code)
|
||||
language_names.append(name)
|
||||
|
||||
# String keys come from English (order matters)
|
||||
english_data = parsed[english_file]
|
||||
string_keys = [k for k in english_data if not k.startswith("_")]
|
||||
|
||||
# Validate all keys are valid C++ identifiers
|
||||
for key in string_keys:
|
||||
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", key):
|
||||
raise ValueError(f"Invalid C++ identifier in English file: '{key}'")
|
||||
|
||||
# Build translations dict, filling missing keys from English
|
||||
translations: Dict[str, List[str]] = {}
|
||||
for key in string_keys:
|
||||
row: List[str] = []
|
||||
for fname in ordered_files:
|
||||
data = parsed[fname]
|
||||
value = data.get(key, "")
|
||||
if not value.strip() and fname != english_file:
|
||||
value = english_data[key]
|
||||
lang_code = parsed[fname].get("_language_code", fname)
|
||||
print(f" INFO: '{key}' missing in {lang_code}, using English fallback")
|
||||
row.append(value)
|
||||
translations[key] = row
|
||||
|
||||
# Warn about extra keys in non-English files
|
||||
for fname in ordered_files:
|
||||
if fname == english_file:
|
||||
continue
|
||||
data = parsed[fname]
|
||||
extra = [k for k in data if not k.startswith("_") and k not in english_data]
|
||||
if extra:
|
||||
lang_code = data.get("_language_code", fname)
|
||||
print(f" WARNING: {lang_code} has keys not in English: {', '.join(extra)}")
|
||||
|
||||
print(f"Loaded {len(language_codes)} languages, {len(string_keys)} string keys")
|
||||
return language_codes, language_names, string_keys, translations
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C++ string escaping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
LANG_ABBREVIATIONS = {
|
||||
"english": "EN",
|
||||
"español": "ES", "espanol": "ES",
|
||||
"italiano": "IT",
|
||||
"svenska": "SV",
|
||||
"français": "FR", "francais": "FR",
|
||||
"deutsch": "DE", "german": "DE",
|
||||
"português": "PT", "portugues": "PT", "português (brasil)": "PO",
|
||||
"中文": "ZH", "chinese": "ZH",
|
||||
"日本語": "JA", "japanese": "JA",
|
||||
"한국어": "KO", "korean": "KO",
|
||||
"русский": "RU", "russian": "RU",
|
||||
"العربية": "AR", "arabic": "AR",
|
||||
"עברית": "HE", "hebrew": "HE",
|
||||
"فارسی": "FA", "persian": "FA",
|
||||
"čeština": "CZ",
|
||||
}
|
||||
|
||||
|
||||
def get_lang_abbreviation(lang_code: str, lang_name: str) -> str:
|
||||
"""Return a 2-letter abbreviation for a language."""
|
||||
lower = lang_name.lower()
|
||||
if lower in LANG_ABBREVIATIONS:
|
||||
return LANG_ABBREVIATIONS[lower]
|
||||
return lang_code[:2].upper()
|
||||
|
||||
|
||||
def escape_cpp_string(s: str) -> List[str]:
|
||||
r"""
|
||||
Convert *s* into one or more C++ string literal segments.
|
||||
|
||||
Non-ASCII characters are emitted as \xNN hex sequences. After each
|
||||
hex escape a new segment is started so the compiler doesn't merge
|
||||
subsequent hex digits into the escape.
|
||||
|
||||
Returns a list of string segments (without quotes). For simple ASCII
|
||||
strings this is a single-element list.
|
||||
"""
|
||||
if not s:
|
||||
return [""]
|
||||
|
||||
s = s.replace("\n", "\\n")
|
||||
|
||||
# Build a flat list of "tokens", where each token is either a regular
|
||||
# character sequence or a hex escape. A segment break happens after
|
||||
# every hex escape.
|
||||
segments: List[str] = []
|
||||
current: List[str] = []
|
||||
i = 0
|
||||
|
||||
def _flush() -> None:
|
||||
segments.append("".join(current))
|
||||
current.clear()
|
||||
|
||||
while i < len(s):
|
||||
ch = s[i]
|
||||
|
||||
if ch == "\\" and i + 1 < len(s):
|
||||
nxt = s[i + 1]
|
||||
if nxt in "ntr\"\\":
|
||||
current.append(ch + nxt)
|
||||
i += 2
|
||||
elif nxt == "x" and i + 3 < len(s):
|
||||
current.append(s[i : i + 4])
|
||||
_flush() # segment break after hex
|
||||
i += 4
|
||||
else:
|
||||
current.append("\\\\")
|
||||
i += 1
|
||||
elif ch == '"':
|
||||
current.append('\\"')
|
||||
i += 1
|
||||
elif ord(ch) < 128:
|
||||
current.append(ch)
|
||||
i += 1
|
||||
else:
|
||||
for byte in ch.encode("utf-8"):
|
||||
current.append(f"\\x{byte:02X}")
|
||||
_flush() # segment break after hex
|
||||
i += 1
|
||||
|
||||
# Flush remaining content
|
||||
_flush()
|
||||
|
||||
return segments
|
||||
|
||||
|
||||
def format_cpp_string_literal(segments: List[str], indent: str = " ") -> List[str]:
|
||||
"""
|
||||
Format string segments (from escape_cpp_string) as indented C++ string
|
||||
literal lines, each wrapped in quotes.
|
||||
Also wraps long segments to respect ~120 column limit.
|
||||
"""
|
||||
# Effective limit for content: 120 - 4 (indent) - 2 (quotes) - 1 (comma/safety) = 113
|
||||
# Using 113 to match clang-format exactly (120 - 4 - 2 - 1)
|
||||
MAX_CONTENT_LEN = 113
|
||||
|
||||
lines: List[str] = []
|
||||
|
||||
for seg in segments:
|
||||
# Short segment (e.g. hex escape or short text)
|
||||
if len(seg) <= MAX_CONTENT_LEN:
|
||||
lines.append(f'{indent}"{seg}"')
|
||||
continue
|
||||
|
||||
# Long segment - wrap it
|
||||
current = seg
|
||||
while len(current) > MAX_CONTENT_LEN:
|
||||
# Find best split point
|
||||
# Scan forward to find last space <= MAX_CONTENT_LEN
|
||||
last_space = -1
|
||||
idx = 0
|
||||
while idx <= MAX_CONTENT_LEN and idx < len(current):
|
||||
if current[idx] == ' ':
|
||||
last_space = idx
|
||||
|
||||
# Handle escapes to step correctly
|
||||
if current[idx] == '\\':
|
||||
idx += 2
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
# If we found a space, split after it
|
||||
if last_space != -1:
|
||||
# Include the space in the first line
|
||||
split_point = last_space + 1
|
||||
lines.append(f'{indent}"{current[:split_point]}"')
|
||||
current = current[split_point:]
|
||||
else:
|
||||
# No space, forced break at MAX_CONTENT_LEN (or slightly less)
|
||||
cut_at = MAX_CONTENT_LEN
|
||||
# Don't cut in the middle of an escape sequence
|
||||
if current[cut_at - 1] == '\\':
|
||||
cut_at -= 1
|
||||
|
||||
lines.append(f'{indent}"{current[:cut_at]}"')
|
||||
current = current[cut_at:]
|
||||
|
||||
if current:
|
||||
lines.append(f'{indent}"{current}"')
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Character-set computation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def compute_character_set(translations: Dict[str, List[str]], lang_index: int) -> str:
|
||||
"""Return a sorted string of every unique character used in a language."""
|
||||
chars = set()
|
||||
for values in translations.values():
|
||||
for ch in values[lang_index]:
|
||||
chars.add(ord(ch))
|
||||
return "".join(chr(cp) for cp in sorted(chars))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Code generators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate_keys_header(
|
||||
languages: List[str],
|
||||
language_names: List[str],
|
||||
string_keys: List[str],
|
||||
output_path: str,
|
||||
) -> None:
|
||||
"""Generate I18nKeys.h."""
|
||||
lines: List[str] = [
|
||||
"#pragma once",
|
||||
"#include <cstdint>",
|
||||
"",
|
||||
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
||||
"",
|
||||
"// Forward declaration for string arrays",
|
||||
"namespace i18n_strings {",
|
||||
]
|
||||
|
||||
for code, name in zip(languages, language_names):
|
||||
abbrev = get_lang_abbreviation(code, name)
|
||||
lines.append(f"extern const char* const STRINGS_{abbrev}[];")
|
||||
|
||||
lines.append("} // namespace i18n_strings")
|
||||
lines.append("")
|
||||
|
||||
# Language enum
|
||||
lines.append("// Language enum")
|
||||
lines.append("enum class Language : uint8_t {")
|
||||
for i, lang in enumerate(languages):
|
||||
lines.append(f" {lang} = {i},")
|
||||
lines.append(" _COUNT")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# Extern declarations
|
||||
lines.append("// Language display names (defined in I18nStrings.cpp)")
|
||||
lines.append("extern const char* const LANGUAGE_NAMES[];")
|
||||
lines.append("")
|
||||
lines.append("// Character sets for each language (defined in I18nStrings.cpp)")
|
||||
lines.append("extern const char* const CHARACTER_SETS[];")
|
||||
lines.append("")
|
||||
|
||||
# StrId enum
|
||||
lines.append("// String IDs")
|
||||
lines.append("enum class StrId : uint16_t {")
|
||||
for key in string_keys:
|
||||
lines.append(f" {key},")
|
||||
lines.append(" // Sentinel - must be last")
|
||||
lines.append(" _COUNT")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# getStringArray helper
|
||||
lines.append("// Helper function to get string array for a language")
|
||||
lines.append("inline const char* const* getStringArray(Language lang) {")
|
||||
lines.append(" switch (lang) {")
|
||||
for code, name in zip(languages, language_names):
|
||||
abbrev = get_lang_abbreviation(code, name)
|
||||
lines.append(f" case Language::{code}:")
|
||||
lines.append(f" return i18n_strings::STRINGS_{abbrev};")
|
||||
first_abbrev = get_lang_abbreviation(languages[0], language_names[0])
|
||||
lines.append(" default:")
|
||||
lines.append(f" return i18n_strings::STRINGS_{first_abbrev};")
|
||||
lines.append(" }")
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
|
||||
# getLanguageCount helper (single line to match checked-in format)
|
||||
lines.append("// Helper function to get language count")
|
||||
lines.append(
|
||||
"constexpr uint8_t getLanguageCount() "
|
||||
"{ return static_cast<uint8_t>(Language::_COUNT); }"
|
||||
)
|
||||
|
||||
_write_file(output_path, lines)
|
||||
|
||||
|
||||
def generate_strings_header(
|
||||
languages: List[str],
|
||||
language_names: List[str],
|
||||
output_path: str,
|
||||
) -> None:
|
||||
"""Generate I18nStrings.h."""
|
||||
lines: List[str] = [
|
||||
"#pragma once",
|
||||
'#include <string>',
|
||||
"",
|
||||
'#include "I18nKeys.h"',
|
||||
"",
|
||||
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
||||
"",
|
||||
"namespace i18n_strings {",
|
||||
"",
|
||||
]
|
||||
|
||||
for code, name in zip(languages, language_names):
|
||||
abbrev = get_lang_abbreviation(code, name)
|
||||
lines.append(f"extern const char* const STRINGS_{abbrev}[];")
|
||||
|
||||
lines.append("")
|
||||
lines.append("} // namespace i18n_strings")
|
||||
|
||||
_write_file(output_path, lines)
|
||||
|
||||
|
||||
def generate_strings_cpp(
|
||||
languages: List[str],
|
||||
language_names: List[str],
|
||||
string_keys: List[str],
|
||||
translations: Dict[str, List[str]],
|
||||
output_path: str,
|
||||
) -> None:
|
||||
"""Generate I18nStrings.cpp."""
|
||||
lines: List[str] = [
|
||||
'#include "I18nStrings.h"',
|
||||
"",
|
||||
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
|
||||
"",
|
||||
]
|
||||
|
||||
# LANGUAGE_NAMES array
|
||||
lines.append("// Language display names")
|
||||
lines.append("const char* const LANGUAGE_NAMES[] = {")
|
||||
for name in language_names:
|
||||
_append_string_entry(lines, name)
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# CHARACTER_SETS array
|
||||
lines.append("// Character sets for each language")
|
||||
lines.append("const char* const CHARACTER_SETS[] = {")
|
||||
for lang_idx, name in enumerate(language_names):
|
||||
charset = compute_character_set(translations, lang_idx)
|
||||
_append_string_entry(lines, charset, comment=name)
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
# Per-language string arrays
|
||||
lines.append("namespace i18n_strings {")
|
||||
lines.append("")
|
||||
|
||||
for lang_idx, (code, name) in enumerate(zip(languages, language_names)):
|
||||
abbrev = get_lang_abbreviation(code, name)
|
||||
lines.append(f"const char* const STRINGS_{abbrev}[] = {{")
|
||||
|
||||
for key in string_keys:
|
||||
text = translations[key][lang_idx]
|
||||
_append_string_entry(lines, text)
|
||||
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
lines.append("} // namespace i18n_strings")
|
||||
lines.append("")
|
||||
|
||||
# Compile-time size checks
|
||||
lines.append("// Compile-time validation of array sizes")
|
||||
for code, name in zip(languages, language_names):
|
||||
abbrev = get_lang_abbreviation(code, name)
|
||||
lines.append(
|
||||
f"static_assert(sizeof(i18n_strings::STRINGS_{abbrev}) "
|
||||
f"/ sizeof(i18n_strings::STRINGS_{abbrev}[0]) =="
|
||||
)
|
||||
lines.append(" static_cast<size_t>(StrId::_COUNT),")
|
||||
lines.append(f' "STRINGS_{abbrev} size mismatch");')
|
||||
|
||||
_write_file(output_path, lines)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _append_string_entry(
|
||||
lines: List[str], text: str, comment: str = ""
|
||||
) -> None:
|
||||
"""Escape *text*, format as indented C++ lines, append comma (and optional comment)."""
|
||||
segments = escape_cpp_string(text)
|
||||
formatted = format_cpp_string_literal(segments)
|
||||
suffix = f", // {comment}" if comment else ","
|
||||
formatted[-1] += suffix
|
||||
lines.extend(formatted)
|
||||
|
||||
|
||||
def _write_file(path: str, lines: List[str]) -> None:
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("\n".join(lines))
|
||||
f.write("\n")
|
||||
print(f"Generated: {path}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main(translations_dir=None, output_dir=None) -> None:
|
||||
# Default paths (relative to project root)
|
||||
default_translations_dir = "lib/I18n/translations"
|
||||
default_output_dir = "lib/I18n/"
|
||||
|
||||
if translations_dir is None or output_dir is None:
|
||||
if len(sys.argv) == 3:
|
||||
translations_dir = sys.argv[1]
|
||||
output_dir = sys.argv[2]
|
||||
else:
|
||||
# Default for no arguments or weird arguments (e.g. SCons)
|
||||
translations_dir = default_translations_dir
|
||||
output_dir = default_output_dir
|
||||
|
||||
|
||||
if not os.path.isdir(translations_dir):
|
||||
print(f"Error: Translations directory not found: {translations_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isdir(output_dir):
|
||||
print(f"Error: Output directory not found: {output_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Reading translations from: {translations_dir}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
try:
|
||||
languages, language_names, string_keys, translations = load_translations(
|
||||
translations_dir
|
||||
)
|
||||
|
||||
out = Path(output_dir)
|
||||
generate_keys_header(languages, language_names, string_keys, str(out / "I18nKeys.h"))
|
||||
generate_strings_header(languages, language_names, str(out / "I18nStrings.h"))
|
||||
generate_strings_cpp(
|
||||
languages, language_names, string_keys, translations, str(out / "I18nStrings.cpp")
|
||||
)
|
||||
|
||||
print()
|
||||
print("✓ Code generation complete!")
|
||||
print(f" Languages: {len(languages)}")
|
||||
print(f" String keys: {len(string_keys)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
try:
|
||||
Import("env")
|
||||
print("Running i18n generation script from PlatformIO...")
|
||||
main()
|
||||
except NameError:
|
||||
pass
|
||||
@@ -33,10 +33,28 @@ def _symbol_from_output(path: pathlib.Path) -> str:
|
||||
|
||||
def write_header(path: pathlib.Path, blob: bytes, symbol: str) -> None:
|
||||
# Emit a constexpr header containing the raw bytes plus a SerializedHyphenationPatterns descriptor.
|
||||
# The binary format has:
|
||||
# - 4 bytes: big-endian root address
|
||||
# - levels tape: from byte 4 to root_addr
|
||||
# - nodes data: from root_addr onwards
|
||||
|
||||
if len(blob) < 4:
|
||||
raise ValueError(f"Blob too small: {len(blob)} bytes")
|
||||
|
||||
# Parse root address (big-endian uint32)
|
||||
root_addr = (blob[0] << 24) | (blob[1] << 16) | (blob[2] << 8) | blob[3]
|
||||
|
||||
if root_addr > len(blob):
|
||||
raise ValueError(f"Root address {root_addr} exceeds blob size {len(blob)}")
|
||||
|
||||
# Remove the 4-byte root address and adjust the offset
|
||||
bytes_literal = _format_bytes(blob[4:])
|
||||
root_addr_new = root_addr - 4
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data_symbol = f"{symbol}_trie_data"
|
||||
patterns_symbol = f"{symbol}_patterns"
|
||||
bytes_literal = _format_bytes(blob)
|
||||
|
||||
content = f"""#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
@@ -50,6 +68,7 @@ alignas(4) constexpr uint8_t {data_symbol}[] = {{
|
||||
}};
|
||||
|
||||
constexpr SerializedHyphenationPatterns {patterns_symbol} = {{
|
||||
{f"0x{root_addr_new:02X}"}u,
|
||||
{data_symbol},
|
||||
sizeof({data_symbol}),
|
||||
}};
|
||||
|
||||
700
scripts/generate_test_epub.py
Normal file
700
scripts/generate_test_epub.py
Normal file
@@ -0,0 +1,700 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate test EPUBs for image rendering verification.
|
||||
|
||||
Creates EPUBs with annotated JPEG and PNG images to verify:
|
||||
- Grayscale rendering (4 levels)
|
||||
- Image scaling
|
||||
- Image centering
|
||||
- Cache performance
|
||||
- Page serialization
|
||||
"""
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except ImportError:
|
||||
print("Please install Pillow: pip install Pillow")
|
||||
exit(1)
|
||||
|
||||
OUTPUT_DIR = Path(__file__).parent.parent / "test" / "epubs"
|
||||
SCREEN_WIDTH = 480
|
||||
SCREEN_HEIGHT = 800
|
||||
|
||||
def get_font(size=20):
|
||||
"""Get a font, falling back to default if needed."""
|
||||
try:
|
||||
return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", size)
|
||||
except:
|
||||
try:
|
||||
return ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSans.ttf", size)
|
||||
except:
|
||||
return ImageFont.load_default()
|
||||
|
||||
def draw_text_centered(draw, y, text, font, fill=0):
|
||||
"""Draw centered text at given y position."""
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
x = (draw.im.size[0] - text_width) // 2
|
||||
draw.text((x, y), text, font=font, fill=fill)
|
||||
|
||||
def draw_text_wrapped(draw, x, y, text, font, max_width, fill=0):
|
||||
"""Draw text with word wrapping."""
|
||||
words = text.split()
|
||||
lines = []
|
||||
current_line = []
|
||||
|
||||
for word in words:
|
||||
test_line = ' '.join(current_line + [word])
|
||||
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||
if bbox[2] - bbox[0] <= max_width:
|
||||
current_line.append(word)
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(' '.join(current_line))
|
||||
current_line = [word]
|
||||
if current_line:
|
||||
lines.append(' '.join(current_line))
|
||||
|
||||
line_height = font.size + 4 if hasattr(font, 'size') else 20
|
||||
for i, line in enumerate(lines):
|
||||
draw.text((x, y + i * line_height), line, font=font, fill=fill)
|
||||
|
||||
return len(lines) * line_height
|
||||
|
||||
def create_grayscale_test_image(filename, is_png=True):
|
||||
"""
|
||||
Create image with 4 grayscale squares to verify 4-level rendering.
|
||||
"""
|
||||
width, height = 400, 600
|
||||
img = Image.new('L', (width, height), 255)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(16)
|
||||
font_small = get_font(14)
|
||||
|
||||
# Title
|
||||
draw_text_centered(draw, 10, "GRAYSCALE TEST", font, fill=0)
|
||||
draw_text_centered(draw, 35, "Verify 4 distinct gray levels", font_small, fill=64)
|
||||
|
||||
# Draw 4 grayscale squares
|
||||
square_size = 70
|
||||
start_y = 65
|
||||
gap = 10
|
||||
|
||||
# Gray levels chosen to avoid Bayer dithering threshold boundaries (±40 dither offset)
|
||||
# Thresholds at 64, 128, 192 - use values in the middle of each band for solid output
|
||||
# Safe zones: 0-23 (black), 88-103 (dark gray), 152-167 (light gray), 232-255 (white)
|
||||
levels = [
|
||||
(0, "Level 0: BLACK"),
|
||||
(96, "Level 1: DARK GRAY"),
|
||||
(160, "Level 2: LIGHT GRAY"),
|
||||
(255, "Level 3: WHITE"),
|
||||
]
|
||||
|
||||
for i, (gray_value, label) in enumerate(levels):
|
||||
y = start_y + i * (square_size + gap + 22)
|
||||
x = (width - square_size) // 2
|
||||
|
||||
# Draw square with border
|
||||
draw.rectangle([x-2, y-2, x + square_size + 2, y + square_size + 2], fill=0)
|
||||
draw.rectangle([x, y, x + square_size, y + square_size], fill=gray_value)
|
||||
|
||||
# Label below square
|
||||
bbox = draw.textbbox((0, 0), label, font=font_small)
|
||||
label_width = bbox[2] - bbox[0]
|
||||
draw.text(((width - label_width) // 2, y + square_size + 5), label, font=font_small, fill=0)
|
||||
|
||||
# Instructions at bottom (well below the last square)
|
||||
y = height - 70
|
||||
draw_text_centered(draw, y, "PASS: 4 distinct shades visible", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 20, "FAIL: Only black/white or", font_small, fill=64)
|
||||
draw_text_centered(draw, y + 38, "muddy/indistinct grays", font_small, fill=64)
|
||||
|
||||
# Save
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_centering_test_image(filename, is_png=True):
|
||||
"""
|
||||
Create image with border markers to verify centering.
|
||||
"""
|
||||
width, height = 350, 400
|
||||
img = Image.new('L', (width, height), 255)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(16)
|
||||
font_small = get_font(14)
|
||||
|
||||
# Draw border
|
||||
draw.rectangle([0, 0, width-1, height-1], outline=0, width=3)
|
||||
|
||||
# Corner markers
|
||||
marker_size = 20
|
||||
for x, y in [(0, 0), (width-marker_size, 0), (0, height-marker_size), (width-marker_size, height-marker_size)]:
|
||||
draw.rectangle([x, y, x+marker_size, y+marker_size], fill=0)
|
||||
|
||||
# Center cross
|
||||
cx, cy = width // 2, height // 2
|
||||
draw.line([cx - 30, cy, cx + 30, cy], fill=0, width=2)
|
||||
draw.line([cx, cy - 30, cx, cy + 30], fill=0, width=2)
|
||||
|
||||
# Title
|
||||
draw_text_centered(draw, 40, "CENTERING TEST", font, fill=0)
|
||||
|
||||
# Instructions
|
||||
y = 80
|
||||
draw_text_centered(draw, y, "Image should be centered", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 20, "horizontally on screen", font_small, fill=0)
|
||||
|
||||
y = 150
|
||||
draw_text_centered(draw, y, "Check:", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 25, "- Equal margins left & right", font_small, fill=64)
|
||||
draw_text_centered(draw, y + 45, "- All 4 corners visible", font_small, fill=64)
|
||||
draw_text_centered(draw, y + 65, "- Border is complete rectangle", font_small, fill=64)
|
||||
|
||||
# Pass/fail
|
||||
y = height - 80
|
||||
draw_text_centered(draw, y, "PASS: Centered, all corners visible", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 20, "FAIL: Off-center or cropped", font_small, fill=64)
|
||||
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_scaling_test_image(filename, is_png=True):
|
||||
"""
|
||||
Create large image to verify scaling works.
|
||||
"""
|
||||
# Make image larger than screen but within decoder limits (max 2048x1536)
|
||||
width, height = 1200, 1500
|
||||
img = Image.new('L', (width, height), 240)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(48)
|
||||
font_medium = get_font(32)
|
||||
font_small = get_font(24)
|
||||
|
||||
# Border
|
||||
draw.rectangle([0, 0, width-1, height-1], outline=0, width=8)
|
||||
draw.rectangle([20, 20, width-21, height-21], outline=128, width=4)
|
||||
|
||||
# Title
|
||||
draw_text_centered(draw, 60, "SCALING TEST", font, fill=0)
|
||||
draw_text_centered(draw, 130, f"Original: {width}x{height} (larger than screen)", font_medium, fill=64)
|
||||
|
||||
# Grid pattern to verify scaling quality
|
||||
grid_start_y = 220
|
||||
grid_size = 400
|
||||
cell_size = 50
|
||||
|
||||
draw_text_centered(draw, grid_start_y - 40, "Grid pattern (check for artifacts):", font_small, fill=0)
|
||||
|
||||
grid_x = (width - grid_size) // 2
|
||||
for row in range(grid_size // cell_size):
|
||||
for col in range(grid_size // cell_size):
|
||||
x = grid_x + col * cell_size
|
||||
y = grid_start_y + row * cell_size
|
||||
if (row + col) % 2 == 0:
|
||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
||||
else:
|
||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200)
|
||||
|
||||
# Size indicator bars
|
||||
y = grid_start_y + grid_size + 60
|
||||
draw_text_centered(draw, y, "Width markers (should fit on screen):", font_small, fill=0)
|
||||
|
||||
bar_y = y + 40
|
||||
# Full width bar
|
||||
draw.rectangle([50, bar_y, width - 50, bar_y + 30], fill=0)
|
||||
draw.text((60, bar_y + 5), "FULL WIDTH", font=font_small, fill=255)
|
||||
|
||||
# Half width bar
|
||||
bar_y += 60
|
||||
half_start = width // 4
|
||||
draw.rectangle([half_start, bar_y, width - half_start, bar_y + 30], fill=85)
|
||||
draw.text((half_start + 10, bar_y + 5), "HALF WIDTH", font=font_small, fill=255)
|
||||
|
||||
# Instructions
|
||||
y = height - 350
|
||||
draw_text_centered(draw, y, "VERIFICATION:", font_medium, fill=0)
|
||||
y += 50
|
||||
instructions = [
|
||||
"1. Image fits within screen bounds",
|
||||
"2. All borders visible (not cropped)",
|
||||
"3. Grid pattern clear (no moire)",
|
||||
"4. Text readable after scaling",
|
||||
"5. Aspect ratio preserved (not stretched)",
|
||||
]
|
||||
for i, text in enumerate(instructions):
|
||||
draw_text_centered(draw, y + i * 35, text, font_small, fill=64)
|
||||
|
||||
y = height - 100
|
||||
draw_text_centered(draw, y, "PASS: Scaled down, readable, complete", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 30, "FAIL: Cropped, distorted, or unreadable", font_small, fill=64)
|
||||
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_wide_scaling_test_image(filename, is_png=True):
|
||||
"""
|
||||
Create wide image (1807x736) to test scaling with specific dimensions
|
||||
that can trigger cache dimension mismatches due to floating-point rounding.
|
||||
"""
|
||||
width, height = 1807, 736
|
||||
img = Image.new('L', (width, height), 240)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(48)
|
||||
font_medium = get_font(32)
|
||||
font_small = get_font(24)
|
||||
|
||||
# Border
|
||||
draw.rectangle([0, 0, width-1, height-1], outline=0, width=6)
|
||||
draw.rectangle([15, 15, width-16, height-16], outline=128, width=3)
|
||||
|
||||
# Title
|
||||
draw_text_centered(draw, 40, "WIDE SCALING TEST", font, fill=0)
|
||||
draw_text_centered(draw, 100, f"Original: {width}x{height} (tests rounding edge case)", font_medium, fill=64)
|
||||
|
||||
# Grid pattern to verify scaling quality
|
||||
grid_start_x = 100
|
||||
grid_start_y = 180
|
||||
grid_width = 600
|
||||
grid_height = 300
|
||||
cell_size = 50
|
||||
|
||||
draw.text((grid_start_x, grid_start_y - 35), "Grid pattern (check for artifacts):", font=font_small, fill=0)
|
||||
|
||||
for row in range(grid_height // cell_size):
|
||||
for col in range(grid_width // cell_size):
|
||||
x = grid_start_x + col * cell_size
|
||||
y = grid_start_y + row * cell_size
|
||||
if (row + col) % 2 == 0:
|
||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
||||
else:
|
||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200)
|
||||
|
||||
# Verification section on the right
|
||||
text_x = 800
|
||||
text_y = 180
|
||||
draw.text((text_x, text_y), "VERIFICATION:", font=font_medium, fill=0)
|
||||
text_y += 50
|
||||
instructions = [
|
||||
"1. Image fits within screen",
|
||||
"2. All borders visible",
|
||||
"3. Grid pattern clear",
|
||||
"4. Text readable",
|
||||
"5. No double-decode in log",
|
||||
]
|
||||
for i, text in enumerate(instructions):
|
||||
draw.text((text_x, text_y + i * 35), text, font=font_small, fill=64)
|
||||
|
||||
# Dimension info
|
||||
draw.text((text_x, 450), f"Dimensions: {width}x{height}", font=font_small, fill=0)
|
||||
draw.text((text_x, 485), "Tests cache dimension matching", font=font_small, fill=64)
|
||||
|
||||
# Pass/fail at bottom
|
||||
y = height - 80
|
||||
draw_text_centered(draw, y, "PASS: Single decode, cached correctly", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 30, "FAIL: Cache mismatch, multiple decodes", font_small, fill=64)
|
||||
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_cache_test_image(filename, page_num, is_png=True):
|
||||
"""
|
||||
Create image for cache performance testing.
|
||||
"""
|
||||
width, height = 400, 300
|
||||
img = Image.new('L', (width, height), 255)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(18)
|
||||
font_small = get_font(14)
|
||||
font_large = get_font(36)
|
||||
|
||||
# Border
|
||||
draw.rectangle([0, 0, width-1, height-1], outline=0, width=2)
|
||||
|
||||
# Page number prominent
|
||||
draw_text_centered(draw, 30, f"CACHE TEST PAGE {page_num}", font, fill=0)
|
||||
draw_text_centered(draw, 80, f"#{page_num}", font_large, fill=0)
|
||||
|
||||
# Instructions
|
||||
y = 140
|
||||
draw_text_centered(draw, y, "Navigate away then return", font_small, fill=64)
|
||||
draw_text_centered(draw, y + 25, "Second load should be faster", font_small, fill=64)
|
||||
|
||||
y = 220
|
||||
draw_text_centered(draw, y, "PASS: Faster reload from cache", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 20, "FAIL: Same slow decode each time", font_small, fill=64)
|
||||
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_gradient_test_image(filename, is_png=True):
|
||||
"""
|
||||
Create horizontal gradient to test grayscale banding.
|
||||
"""
|
||||
width, height = 400, 500
|
||||
img = Image.new('L', (width, height), 255)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(16)
|
||||
font_small = get_font(14)
|
||||
|
||||
draw_text_centered(draw, 10, "GRADIENT TEST", font, fill=0)
|
||||
draw_text_centered(draw, 35, "Smooth gradient → 4 bands expected", font_small, fill=64)
|
||||
|
||||
# Horizontal gradient
|
||||
gradient_y = 70
|
||||
gradient_height = 100
|
||||
for x in range(width):
|
||||
gray = int(255 * x / width)
|
||||
draw.line([(x, gradient_y), (x, gradient_y + gradient_height)], fill=gray)
|
||||
|
||||
# Border around gradient
|
||||
draw.rectangle([0, gradient_y-1, width-1, gradient_y + gradient_height + 1], outline=0, width=1)
|
||||
|
||||
# Labels
|
||||
y = gradient_y + gradient_height + 10
|
||||
draw.text((5, y), "BLACK", font=font_small, fill=0)
|
||||
draw.text((width - 50, y), "WHITE", font=font_small, fill=0)
|
||||
|
||||
# 4-step gradient (what it should look like)
|
||||
y = 220
|
||||
draw_text_centered(draw, y, "Expected result (4 distinct bands):", font_small, fill=0)
|
||||
|
||||
band_y = y + 25
|
||||
band_height = 60
|
||||
band_width = width // 4
|
||||
for i, gray in enumerate([0, 85, 170, 255]):
|
||||
x = i * band_width
|
||||
draw.rectangle([x, band_y, x + band_width, band_y + band_height], fill=gray)
|
||||
draw.rectangle([0, band_y-1, width-1, band_y + band_height + 1], outline=0, width=1)
|
||||
|
||||
# Vertical gradient
|
||||
y = 340
|
||||
draw_text_centered(draw, y, "Vertical gradient:", font_small, fill=0)
|
||||
|
||||
vgrad_y = y + 25
|
||||
vgrad_height = 80
|
||||
for row in range(vgrad_height):
|
||||
gray = int(255 * row / vgrad_height)
|
||||
draw.line([(50, vgrad_y + row), (width - 50, vgrad_y + row)], fill=gray)
|
||||
draw.rectangle([49, vgrad_y-1, width-49, vgrad_y + vgrad_height + 1], outline=0, width=1)
|
||||
|
||||
# Pass/fail
|
||||
y = height - 50
|
||||
draw_text_centered(draw, y, "PASS: Clear 4-band quantization", font_small, fill=0)
|
||||
draw_text_centered(draw, y + 20, "FAIL: Binary/noisy dithering", font_small, fill=64)
|
||||
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_format_test_image(filename, format_name, is_png=True):
|
||||
"""
|
||||
Create simple image to verify format support.
|
||||
"""
|
||||
width, height = 350, 250
|
||||
img = Image.new('L', (width, height), 255)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(20)
|
||||
font_large = get_font(36)
|
||||
font_small = get_font(14)
|
||||
|
||||
# Border
|
||||
draw.rectangle([0, 0, width-1, height-1], outline=0, width=3)
|
||||
|
||||
# Format name
|
||||
draw_text_centered(draw, 30, f"{format_name} FORMAT TEST", font, fill=0)
|
||||
draw_text_centered(draw, 80, format_name, font_large, fill=0)
|
||||
|
||||
# Checkmark area
|
||||
y = 140
|
||||
draw_text_centered(draw, y, "If you can read this,", font_small, fill=64)
|
||||
draw_text_centered(draw, y + 20, f"{format_name} decoding works!", font_small, fill=64)
|
||||
|
||||
y = height - 40
|
||||
draw_text_centered(draw, y, f"PASS: {format_name} image visible", font_small, fill=0)
|
||||
|
||||
if is_png:
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
def create_epub(epub_path, title, chapters):
|
||||
"""
|
||||
Create an EPUB file with the given chapters.
|
||||
|
||||
chapters: list of (chapter_title, html_content, images)
|
||||
images: list of (image_filename, image_data)
|
||||
"""
|
||||
with zipfile.ZipFile(epub_path, 'w', zipfile.ZIP_DEFLATED) as epub:
|
||||
# mimetype (must be first, uncompressed)
|
||||
epub.writestr('mimetype', 'application/epub+zip', compress_type=zipfile.ZIP_STORED)
|
||||
|
||||
# Container
|
||||
container_xml = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||
</rootfiles>
|
||||
</container>'''
|
||||
epub.writestr('META-INF/container.xml', container_xml)
|
||||
|
||||
# Collect all images and chapters
|
||||
manifest_items = []
|
||||
spine_items = []
|
||||
|
||||
# Add chapters and images
|
||||
for i, (chapter_title, html_content, images) in enumerate(chapters):
|
||||
chapter_id = f'chapter{i+1}'
|
||||
chapter_file = f'chapter{i+1}.xhtml'
|
||||
|
||||
# Add images for this chapter
|
||||
for img_filename, img_data in images:
|
||||
media_type = 'image/png' if img_filename.endswith('.png') else 'image/jpeg'
|
||||
manifest_items.append(f' <item id="{img_filename.replace(".", "_")}" href="images/{img_filename}" media-type="{media_type}"/>')
|
||||
epub.writestr(f'OEBPS/images/{img_filename}', img_data)
|
||||
|
||||
# Add chapter
|
||||
manifest_items.append(f' <item id="{chapter_id}" href="{chapter_file}" media-type="application/xhtml+xml"/>')
|
||||
spine_items.append(f' <itemref idref="{chapter_id}"/>')
|
||||
epub.writestr(f'OEBPS/{chapter_file}', html_content)
|
||||
|
||||
# content.opf
|
||||
content_opf = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:identifier id="uid">test-epub-{title.lower().replace(" ", "-")}</dc:identifier>
|
||||
<dc:title>{title}</dc:title>
|
||||
<dc:language>en</dc:language>
|
||||
</metadata>
|
||||
<manifest>
|
||||
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
|
||||
{chr(10).join(manifest_items)}
|
||||
</manifest>
|
||||
<spine>
|
||||
{chr(10).join(spine_items)}
|
||||
</spine>
|
||||
</package>'''
|
||||
epub.writestr('OEBPS/content.opf', content_opf)
|
||||
|
||||
# Navigation document
|
||||
nav_items = '\n'.join([f' <li><a href="chapter{i+1}.xhtml">{chapters[i][0]}</a></li>'
|
||||
for i in range(len(chapters))])
|
||||
nav_xhtml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
|
||||
<head><title>Navigation</title></head>
|
||||
<body>
|
||||
<nav epub:type="toc">
|
||||
<h1>Contents</h1>
|
||||
<ol>
|
||||
{nav_items}
|
||||
</ol>
|
||||
</nav>
|
||||
</body>
|
||||
</html>'''
|
||||
epub.writestr('OEBPS/nav.xhtml', nav_xhtml)
|
||||
|
||||
def make_chapter(title, body_content):
|
||||
"""Create XHTML chapter content."""
|
||||
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>{title}</title></head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
{body_content}
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
def main():
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Temp directory for images
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir = Path(tmpdir)
|
||||
|
||||
print("Generating test images...")
|
||||
|
||||
# Generate all test images
|
||||
images = {}
|
||||
|
||||
# JPEG tests
|
||||
create_grayscale_test_image(tmpdir / 'grayscale_test.jpg', is_png=False)
|
||||
create_centering_test_image(tmpdir / 'centering_test.jpg', is_png=False)
|
||||
create_scaling_test_image(tmpdir / 'scaling_test.jpg', is_png=False)
|
||||
create_wide_scaling_test_image(tmpdir / 'wide_scaling_test.jpg', is_png=False)
|
||||
create_gradient_test_image(tmpdir / 'gradient_test.jpg', is_png=False)
|
||||
create_format_test_image(tmpdir / 'jpeg_format.jpg', 'JPEG', is_png=False)
|
||||
create_cache_test_image(tmpdir / 'cache_test_1.jpg', 1, is_png=False)
|
||||
create_cache_test_image(tmpdir / 'cache_test_2.jpg', 2, is_png=False)
|
||||
|
||||
# PNG tests
|
||||
create_grayscale_test_image(tmpdir / 'grayscale_test.png', is_png=True)
|
||||
create_centering_test_image(tmpdir / 'centering_test.png', is_png=True)
|
||||
create_scaling_test_image(tmpdir / 'scaling_test.png', is_png=True)
|
||||
create_wide_scaling_test_image(tmpdir / 'wide_scaling_test.png', is_png=True)
|
||||
create_gradient_test_image(tmpdir / 'gradient_test.png', is_png=True)
|
||||
create_format_test_image(tmpdir / 'png_format.png', 'PNG', is_png=True)
|
||||
create_cache_test_image(tmpdir / 'cache_test_1.png', 1, is_png=True)
|
||||
create_cache_test_image(tmpdir / 'cache_test_2.png', 2, is_png=True)
|
||||
|
||||
# Read all images
|
||||
for img_file in tmpdir.glob('*.*'):
|
||||
images[img_file.name] = img_file.read_bytes()
|
||||
|
||||
print("Creating JPEG test EPUB...")
|
||||
jpeg_chapters = [
|
||||
("Introduction", make_chapter("JPEG Image Tests", """
|
||||
<p>This EPUB tests JPEG image rendering.</p>
|
||||
<p>Navigate through chapters to verify each test case.</p>
|
||||
<p><strong>Test Plan:</strong></p>
|
||||
<ul>
|
||||
<li>Grayscale rendering (4 levels)</li>
|
||||
<li>Image centering</li>
|
||||
<li>Large image scaling</li>
|
||||
<li>Cache performance</li>
|
||||
</ul>
|
||||
"""), []),
|
||||
("1. JPEG Format", make_chapter("JPEG Format Test", """
|
||||
<p>Basic JPEG decoding test.</p>
|
||||
<img src="images/jpeg_format.jpg" alt="JPEG format test"/>
|
||||
<p>If the image above is visible, JPEG decoding works.</p>
|
||||
"""), [('jpeg_format.jpg', images['jpeg_format.jpg'])]),
|
||||
("2. Grayscale", make_chapter("Grayscale Test", """
|
||||
<p>Verify 4 distinct gray levels are visible.</p>
|
||||
<img src="images/grayscale_test.jpg" alt="Grayscale test"/>
|
||||
"""), [('grayscale_test.jpg', images['grayscale_test.jpg'])]),
|
||||
("3. Gradient", make_chapter("Gradient Test", """
|
||||
<p>Verify gradient quantizes to 4 bands.</p>
|
||||
<img src="images/gradient_test.jpg" alt="Gradient test"/>
|
||||
"""), [('gradient_test.jpg', images['gradient_test.jpg'])]),
|
||||
("4. Centering", make_chapter("Centering Test", """
|
||||
<p>Verify image is centered horizontally.</p>
|
||||
<img src="images/centering_test.jpg" alt="Centering test"/>
|
||||
"""), [('centering_test.jpg', images['centering_test.jpg'])]),
|
||||
("5. Scaling", make_chapter("Scaling Test", """
|
||||
<p>This image is 1200x1500 pixels - larger than the screen.</p>
|
||||
<p>It should be scaled down to fit.</p>
|
||||
<img src="images/scaling_test.jpg" alt="Scaling test"/>
|
||||
"""), [('scaling_test.jpg', images['scaling_test.jpg'])]),
|
||||
("6. Wide Scaling", make_chapter("Wide Scaling Test", """
|
||||
<p>This image is 1807x736 pixels - a wide landscape format.</p>
|
||||
<p>Tests scaling with dimensions that can cause cache mismatches.</p>
|
||||
<img src="images/wide_scaling_test.jpg" alt="Wide scaling test"/>
|
||||
"""), [('wide_scaling_test.jpg', images['wide_scaling_test.jpg'])]),
|
||||
("7. Cache Test A", make_chapter("Cache Test - Page A", """
|
||||
<p>First cache test page. Note the load time.</p>
|
||||
<img src="images/cache_test_1.jpg" alt="Cache test 1"/>
|
||||
<p>Navigate to next page, then come back.</p>
|
||||
"""), [('cache_test_1.jpg', images['cache_test_1.jpg'])]),
|
||||
("8. Cache Test B", make_chapter("Cache Test - Page B", """
|
||||
<p>Second cache test page.</p>
|
||||
<img src="images/cache_test_2.jpg" alt="Cache test 2"/>
|
||||
<p>Navigate back to Page A - it should load faster from cache.</p>
|
||||
"""), [('cache_test_2.jpg', images['cache_test_2.jpg'])]),
|
||||
]
|
||||
|
||||
create_epub(OUTPUT_DIR / 'test_jpeg_images.epub', 'JPEG Image Tests', jpeg_chapters)
|
||||
|
||||
print("Creating PNG test EPUB...")
|
||||
png_chapters = [
|
||||
("Introduction", make_chapter("PNG Image Tests", """
|
||||
<p>This EPUB tests PNG image rendering.</p>
|
||||
<p>Navigate through chapters to verify each test case.</p>
|
||||
<p><strong>Test Plan:</strong></p>
|
||||
<ul>
|
||||
<li>PNG decoding (no crash)</li>
|
||||
<li>Grayscale rendering (4 levels)</li>
|
||||
<li>Image centering</li>
|
||||
<li>Large image scaling</li>
|
||||
</ul>
|
||||
"""), []),
|
||||
("1. PNG Format", make_chapter("PNG Format Test", """
|
||||
<p>Basic PNG decoding test.</p>
|
||||
<img src="images/png_format.png" alt="PNG format test"/>
|
||||
<p>If the image above is visible and no crash occurred, PNG decoding works.</p>
|
||||
"""), [('png_format.png', images['png_format.png'])]),
|
||||
("2. Grayscale", make_chapter("Grayscale Test", """
|
||||
<p>Verify 4 distinct gray levels are visible.</p>
|
||||
<img src="images/grayscale_test.png" alt="Grayscale test"/>
|
||||
"""), [('grayscale_test.png', images['grayscale_test.png'])]),
|
||||
("3. Gradient", make_chapter("Gradient Test", """
|
||||
<p>Verify gradient quantizes to 4 bands.</p>
|
||||
<img src="images/gradient_test.png" alt="Gradient test"/>
|
||||
"""), [('gradient_test.png', images['gradient_test.png'])]),
|
||||
("4. Centering", make_chapter("Centering Test", """
|
||||
<p>Verify image is centered horizontally.</p>
|
||||
<img src="images/centering_test.png" alt="Centering test"/>
|
||||
"""), [('centering_test.png', images['centering_test.png'])]),
|
||||
("5. Scaling", make_chapter("Scaling Test", """
|
||||
<p>This image is 1200x1500 pixels - larger than the screen.</p>
|
||||
<p>It should be scaled down to fit.</p>
|
||||
<img src="images/scaling_test.png" alt="Scaling test"/>
|
||||
"""), [('scaling_test.png', images['scaling_test.png'])]),
|
||||
("6. Wide Scaling", make_chapter("Wide Scaling Test", """
|
||||
<p>This image is 1807x736 pixels - a wide landscape format.</p>
|
||||
<p>Tests scaling with dimensions that can cause cache mismatches.</p>
|
||||
<img src="images/wide_scaling_test.png" alt="Wide scaling test"/>
|
||||
"""), [('wide_scaling_test.png', images['wide_scaling_test.png'])]),
|
||||
("7. Cache Test A", make_chapter("Cache Test - Page A", """
|
||||
<p>First cache test page. Note the load time.</p>
|
||||
<img src="images/cache_test_1.png" alt="Cache test 1"/>
|
||||
<p>Navigate to next page, then come back.</p>
|
||||
"""), [('cache_test_1.png', images['cache_test_1.png'])]),
|
||||
("8. Cache Test B", make_chapter("Cache Test - Page B", """
|
||||
<p>Second cache test page.</p>
|
||||
<img src="images/cache_test_2.png" alt="Cache test 2"/>
|
||||
<p>Navigate back to Page A - it should load faster from cache.</p>
|
||||
"""), [('cache_test_2.png', images['cache_test_2.png'])]),
|
||||
]
|
||||
|
||||
create_epub(OUTPUT_DIR / 'test_png_images.epub', 'PNG Image Tests', png_chapters)
|
||||
|
||||
print("Creating mixed format test EPUB...")
|
||||
mixed_chapters = [
|
||||
("Introduction", make_chapter("Mixed Image Format Tests", """
|
||||
<p>This EPUB contains both JPEG and PNG images.</p>
|
||||
<p>Tests format detection and mixed rendering.</p>
|
||||
"""), []),
|
||||
("1. JPEG Image", make_chapter("JPEG in Mixed EPUB", """
|
||||
<p>This is a JPEG image:</p>
|
||||
<img src="images/jpeg_format.jpg" alt="JPEG"/>
|
||||
"""), [('jpeg_format.jpg', images['jpeg_format.jpg'])]),
|
||||
("2. PNG Image", make_chapter("PNG in Mixed EPUB", """
|
||||
<p>This is a PNG image:</p>
|
||||
<img src="images/png_format.png" alt="PNG"/>
|
||||
"""), [('png_format.png', images['png_format.png'])]),
|
||||
("3. Both Formats", make_chapter("Both Formats on One Page", """
|
||||
<p>JPEG image:</p>
|
||||
<img src="images/grayscale_test.jpg" alt="JPEG grayscale"/>
|
||||
<p>PNG image:</p>
|
||||
<img src="images/grayscale_test.png" alt="PNG grayscale"/>
|
||||
<p>Both should render with proper grayscale.</p>
|
||||
"""), [('grayscale_test.jpg', images['grayscale_test.jpg']),
|
||||
('grayscale_test.png', images['grayscale_test.png'])]),
|
||||
]
|
||||
|
||||
create_epub(OUTPUT_DIR / 'test_mixed_images.epub', 'Mixed Format Tests', mixed_chapters)
|
||||
|
||||
print(f"\nTest EPUBs created in: {OUTPUT_DIR}")
|
||||
print("Files:")
|
||||
for f in OUTPUT_DIR.glob('*.epub'):
|
||||
print(f" - {f.name}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
24
scripts/update_hypenation.sh
Executable file
24
scripts/update_hypenation.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
process() {
|
||||
local lang="$1"
|
||||
|
||||
mkdir -p "build"
|
||||
wget -O "build/$lang.bin" "https://github.com/typst/hypher/raw/refs/heads/main/tries/$lang.bin"
|
||||
|
||||
python scripts/generate_hyphenation_trie.py \
|
||||
--input "build/$lang.bin" \
|
||||
--output "lib/Epub/Epub/hyphenation/generated/hyph-${lang}.trie.h"
|
||||
}
|
||||
|
||||
process en
|
||||
process fr
|
||||
process de
|
||||
process es
|
||||
process ru
|
||||
process it
|
||||
@@ -1,10 +1,11 @@
|
||||
#include "CrossPointSettings.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -21,8 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
// Increment this when adding new persisted settings fields
|
||||
constexpr uint8_t SETTINGS_COUNT = 30;
|
||||
// SETTINGS_COUNT is now calculated automatically in saveToFile
|
||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||
|
||||
// Validate front button mapping to ensure each hardware button is unique.
|
||||
@@ -77,64 +77,102 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class SettingsWriter {
|
||||
public:
|
||||
bool is_counting = false;
|
||||
uint8_t item_count = 0;
|
||||
template <typename T>
|
||||
|
||||
void writeItem(FsFile& file, const T& value) {
|
||||
if (is_counting) {
|
||||
item_count++;
|
||||
} else {
|
||||
serialization::writePod(file, value);
|
||||
}
|
||||
}
|
||||
|
||||
void writeItemString(FsFile& file, const char* value) {
|
||||
if (is_counting) {
|
||||
item_count++;
|
||||
} else {
|
||||
serialization::writeString(file, std::string(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const {
|
||||
SettingsWriter writer;
|
||||
writer.is_counting = count_only;
|
||||
|
||||
writer.writeItem(file, sleepScreen);
|
||||
writer.writeItem(file, extraParagraphSpacing);
|
||||
writer.writeItem(file, shortPwrBtn);
|
||||
writer.writeItem(file, statusBar);
|
||||
writer.writeItem(file, orientation);
|
||||
writer.writeItem(file, frontButtonLayout); // legacy
|
||||
writer.writeItem(file, sideButtonLayout);
|
||||
writer.writeItem(file, fontFamily);
|
||||
writer.writeItem(file, fontSize);
|
||||
writer.writeItem(file, lineSpacing);
|
||||
writer.writeItem(file, paragraphAlignment);
|
||||
writer.writeItem(file, sleepTimeout);
|
||||
writer.writeItem(file, refreshFrequency);
|
||||
writer.writeItem(file, screenMargin);
|
||||
writer.writeItem(file, sleepScreenCoverMode);
|
||||
writer.writeItemString(file, opdsServerUrl);
|
||||
writer.writeItem(file, textAntiAliasing);
|
||||
writer.writeItem(file, hideBatteryPercentage);
|
||||
writer.writeItem(file, longPressChapterSkip);
|
||||
writer.writeItem(file, hyphenationEnabled);
|
||||
writer.writeItemString(file, opdsUsername);
|
||||
writer.writeItemString(file, opdsPassword);
|
||||
writer.writeItem(file, sleepScreenCoverFilter);
|
||||
writer.writeItem(file, uiTheme);
|
||||
writer.writeItem(file, frontButtonBack);
|
||||
writer.writeItem(file, frontButtonConfirm);
|
||||
writer.writeItem(file, frontButtonLeft);
|
||||
writer.writeItem(file, frontButtonRight);
|
||||
writer.writeItem(file, fadingFix);
|
||||
writer.writeItem(file, embeddedStyle);
|
||||
// New fields need to be added at end for backward compatibility
|
||||
|
||||
return writer.item_count;
|
||||
}
|
||||
|
||||
bool CrossPointSettings::saveToFile() const {
|
||||
// Make sure the directory exists
|
||||
SdMan.mkdir("/.crosspoint");
|
||||
Storage.mkdir("/.crosspoint");
|
||||
|
||||
FsFile outputFile;
|
||||
if (!SdMan.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) {
|
||||
if (!Storage.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First pass: count the items
|
||||
uint8_t item_count = writeSettings(outputFile, true); // This will just count, not write
|
||||
|
||||
// Write header
|
||||
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
||||
serialization::writePod(outputFile, SETTINGS_COUNT);
|
||||
serialization::writePod(outputFile, sleepScreen);
|
||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
||||
serialization::writePod(outputFile, shortPwrBtn);
|
||||
serialization::writePod(outputFile, statusBar);
|
||||
serialization::writePod(outputFile, orientation);
|
||||
serialization::writePod(outputFile, frontButtonLayout); // legacy
|
||||
serialization::writePod(outputFile, sideButtonLayout);
|
||||
serialization::writePod(outputFile, fontFamily);
|
||||
serialization::writePod(outputFile, fontSize);
|
||||
serialization::writePod(outputFile, lineSpacing);
|
||||
serialization::writePod(outputFile, paragraphAlignment);
|
||||
serialization::writePod(outputFile, sleepTimeout);
|
||||
serialization::writePod(outputFile, refreshFrequency);
|
||||
serialization::writePod(outputFile, screenMargin);
|
||||
serialization::writePod(outputFile, sleepScreenCoverMode);
|
||||
serialization::writeString(outputFile, std::string(opdsServerUrl));
|
||||
serialization::writePod(outputFile, textAntiAliasing);
|
||||
serialization::writePod(outputFile, hideBatteryPercentage);
|
||||
serialization::writePod(outputFile, longPressChapterSkip);
|
||||
serialization::writePod(outputFile, hyphenationEnabled);
|
||||
serialization::writeString(outputFile, std::string(opdsUsername));
|
||||
serialization::writeString(outputFile, std::string(opdsPassword));
|
||||
serialization::writePod(outputFile, sleepScreenCoverFilter);
|
||||
serialization::writePod(outputFile, uiTheme);
|
||||
serialization::writePod(outputFile, frontButtonBack);
|
||||
serialization::writePod(outputFile, frontButtonConfirm);
|
||||
serialization::writePod(outputFile, frontButtonLeft);
|
||||
serialization::writePod(outputFile, frontButtonRight);
|
||||
serialization::writePod(outputFile, fadingFix);
|
||||
serialization::writePod(outputFile, embeddedStyle);
|
||||
// New fields added at end for backward compatibility
|
||||
serialization::writePod(outputFile, static_cast<uint8_t>(item_count));
|
||||
// Second pass: actually write the settings
|
||||
writeSettings(outputFile); // This will write the actual data
|
||||
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
LOG_DBG("CPS", "Settings saved to file");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CrossPointSettings::loadFromFile() {
|
||||
FsFile inputFile;
|
||||
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
||||
if (!Storage.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(inputFile, version);
|
||||
if (version != SETTINGS_FILE_VERSION) {
|
||||
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version);
|
||||
inputFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -233,7 +271,7 @@ bool CrossPointSettings::loadFromFile() {
|
||||
}
|
||||
|
||||
inputFile.close();
|
||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
||||
LOG_DBG("CPS", "Settings loaded from file");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
|
||||
// Forward declarations
|
||||
class FsFile;
|
||||
|
||||
class CrossPointSettings {
|
||||
private:
|
||||
// Private constructor for singleton
|
||||
@@ -182,6 +185,9 @@ class CrossPointSettings {
|
||||
}
|
||||
int getReaderFontId() const;
|
||||
|
||||
// If count_only is true, returns the number of settings items that would be written.
|
||||
uint8_t writeSettings(FsFile& file, bool count_only = false) const;
|
||||
|
||||
bool saveToFile() const;
|
||||
bool loadFromFile();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "CrossPointState.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
namespace {
|
||||
@@ -13,7 +13,7 @@ CrossPointState CrossPointState::instance;
|
||||
|
||||
bool CrossPointState::saveToFile() const {
|
||||
FsFile outputFile;
|
||||
if (!SdMan.openFileForWrite("CPS", STATE_FILE, outputFile)) {
|
||||
if (!Storage.openFileForWrite("CPS", STATE_FILE, outputFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@ bool CrossPointState::saveToFile() const {
|
||||
|
||||
bool CrossPointState::loadFromFile() {
|
||||
FsFile inputFile;
|
||||
if (!SdMan.openFileForRead("CPS", STATE_FILE, inputFile)) {
|
||||
if (!Storage.openFileForRead("CPS", STATE_FILE, inputFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(inputFile, version);
|
||||
if (version > STATE_FILE_VERSION) {
|
||||
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version);
|
||||
inputFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class MappedInputManager {
|
||||
|
||||
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
|
||||
|
||||
void update() const { gpio.update(); }
|
||||
bool wasPressed(Button button) const;
|
||||
bool wasReleased(Button button) const;
|
||||
bool isPressed(Button button) const;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user