Compare commits
19 Commits
mod/master
...
97c33141bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c33141bd | ||
|
|
2a32d8a182 | ||
|
|
d6f38d4441 | ||
|
|
513d111634 | ||
|
|
ad9137cfdf | ||
|
|
5c80cface7 | ||
|
|
86d3774a8f | ||
|
|
7ba5978848 | ||
|
|
3d47c081f2 | ||
|
|
6702060960 | ||
|
|
0bc6747483 | ||
|
|
00666377de | ||
|
|
22b77edddf | ||
|
|
2e673c753d | ||
|
|
1a30826981 | ||
|
|
50e6ef9bd8 | ||
|
|
a616f42cb4 | ||
|
|
0508bfc1f7 | ||
|
|
6c3a615fac |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,14 +3,10 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nStrings.cpp
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
|
||||
# mod
|
||||
mod/*
|
||||
.cursor/*
|
||||
chat-summaries/*
|
||||
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef OMIT_BOOKERLY
|
||||
#include <builtinFonts/bookerly_12_bold.h>
|
||||
#include <builtinFonts/bookerly_12_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_12_italic.h>
|
||||
@@ -17,10 +16,7 @@
|
||||
#include <builtinFonts/bookerly_18_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_18_italic.h>
|
||||
#include <builtinFonts/bookerly_18_regular.h>
|
||||
#endif // OMIT_BOOKERLY
|
||||
|
||||
#include <builtinFonts/notosans_8_regular.h>
|
||||
#ifndef OMIT_NOTOSANS
|
||||
#include <builtinFonts/notosans_12_bold.h>
|
||||
#include <builtinFonts/notosans_12_bolditalic.h>
|
||||
#include <builtinFonts/notosans_12_italic.h>
|
||||
@@ -37,9 +33,6 @@
|
||||
#include <builtinFonts/notosans_18_bolditalic.h>
|
||||
#include <builtinFonts/notosans_18_italic.h>
|
||||
#include <builtinFonts/notosans_18_regular.h>
|
||||
#endif // OMIT_NOTOSANS
|
||||
|
||||
#ifndef OMIT_OPENDYSLEXIC
|
||||
#include <builtinFonts/opendyslexic_10_bold.h>
|
||||
#include <builtinFonts/opendyslexic_10_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_10_italic.h>
|
||||
@@ -56,8 +49,6 @@
|
||||
#include <builtinFonts/opendyslexic_8_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_8_italic.h>
|
||||
#include <builtinFonts/opendyslexic_8_regular.h>
|
||||
#endif // OMIT_OPENDYSLEXIC
|
||||
|
||||
#include <builtinFonts/ubuntu_10_bold.h>
|
||||
#include <builtinFonts/ubuntu_10_regular.h>
|
||||
#include <builtinFonts/ubuntu_12_bold.h>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
#include <PngToBmpConverter.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Epub/parsers/ContainerParser.h"
|
||||
#include "Epub/parsers/ContentOpfParser.h"
|
||||
#include "Epub/parsers/TocNavParser.h"
|
||||
@@ -79,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()) {
|
||||
@@ -443,19 +489,10 @@ std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||
}
|
||||
|
||||
bool Epub::generateCoverBmp(bool cropped) const {
|
||||
bool invalid = false;
|
||||
// Already generated, return true
|
||||
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||
// is this a valid cover or just an empty file we created to mark generation attempts?
|
||||
invalid = !isValidThumbnailBmp(getCoverBmpPath(cropped));
|
||||
if (invalid) {
|
||||
// Remove the old invalid cover so we can attempt to generate a new one
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
LOG_DBG("EBP", "Previous cover generation attempt failed for %s mode, retrying", cropped ? "cropped" : "fit");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||
@@ -463,33 +500,13 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
std::string effectiveCoverImageHref = coverImageHref;
|
||||
if (coverImageHref.empty()) {
|
||||
// Fallback: try common cover filenames
|
||||
std::vector<std::string> coverCandidates = getCoverCandidates();
|
||||
for (const auto& candidate : coverCandidates) {
|
||||
effectiveCoverImageHref = candidate;
|
||||
// Try to read a small amount to check if exists
|
||||
uint8_t* test = readItemContentsToBytes(candidate, nullptr, false);
|
||||
if (test) {
|
||||
free(test);
|
||||
break;
|
||||
} else {
|
||||
effectiveCoverImageHref.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (effectiveCoverImageHref.empty()) {
|
||||
LOG_ERR("EBP", "No known cover image");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for JPG/JPEG extensions (case insensitive)
|
||||
std::string lowerHref = effectiveCoverImageHref;
|
||||
std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower);
|
||||
bool isJpg =
|
||||
lowerHref.substr(lowerHref.length() - 4) == ".jpg" || lowerHref.substr(lowerHref.length() - 5) == ".jpeg";
|
||||
if (isJpg) {
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
@@ -497,7 +514,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(effectiveCoverImageHref, coverJpg, 1024);
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
@@ -518,12 +535,44 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated BMP from cover image, success: %s", success ? "yes" : "no");
|
||||
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -531,19 +580,10 @@ std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].
|
||||
std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||
|
||||
bool Epub::generateThumbBmp(int height) const {
|
||||
bool invalid = false;
|
||||
// Already generated, return true
|
||||
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||
// is this a valid thumbnail or just an empty file we created to mark generation attempts?
|
||||
invalid = !isValidThumbnailBmp(getThumbBmpPath(height));
|
||||
if (invalid) {
|
||||
// Remove the old invalid thumbnail so we can attempt to generate a new one
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
LOG_DBG("EBP", "Previous thumbnail generation attempt failed for height %d, retrying", height);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||
@@ -551,31 +591,10 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
std::string effectiveCoverImageHref = coverImageHref;
|
||||
if (coverImageHref.empty()) {
|
||||
// Fallback: try common cover filenames
|
||||
std::vector<std::string> coverCandidates = getCoverCandidates();
|
||||
for (const auto& candidate : coverCandidates) {
|
||||
effectiveCoverImageHref = candidate;
|
||||
// Try to read a small amount to check if exists
|
||||
uint8_t* test = readItemContentsToBytes(candidate, nullptr, false);
|
||||
if (test) {
|
||||
free(test);
|
||||
break;
|
||||
} else {
|
||||
effectiveCoverImageHref.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (effectiveCoverImageHref.empty()) {
|
||||
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||
} else {
|
||||
// Check for JPG/JPEG extensions (case insensitive)
|
||||
std::string lowerHref = effectiveCoverImageHref;
|
||||
std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower);
|
||||
bool isJpg =
|
||||
lowerHref.substr(lowerHref.length() - 4) == ".jpg" || lowerHref.substr(lowerHref.length() - 5) == ".jpeg";
|
||||
if (isJpg) {
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
@@ -583,7 +602,7 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(effectiveCoverImageHref, coverJpg, 1024);
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
@@ -611,186 +630,51 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
|
||||
bool Epub::generateInvalidFormatThumbBmp(int height) const {
|
||||
// Create a simple 1-bit BMP with an X pattern to indicate invalid format.
|
||||
// This BMP is a valid 1-bit file used as a marker to prevent repeated
|
||||
// generation attempts when conversion fails (e.g., progressive JPG).
|
||||
const int width = height * 0.6; // Same aspect ratio as normal thumbnails
|
||||
const int rowBytes = ((width + 31) / 32) * 4; // 1-bit rows padded to 4-byte boundary
|
||||
const int imageSize = rowBytes * height;
|
||||
const int fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data
|
||||
const int dataOffset = 14 + 40 + 8;
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
coverPng.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// BMP file header (14 bytes)
|
||||
thumbBmp.write('B');
|
||||
thumbBmp.write('M');
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
// DIB header (BITMAPINFOHEADER - 40 bytes)
|
||||
uint32_t dibHeaderSize = 40;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t bmpWidth = width;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
|
||||
int32_t bmpHeight = -height; // Negative for top-down
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
|
||||
uint16_t planes = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835; // 72 DPI
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
// Color palette (2 colors for 1-bit)
|
||||
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; // Color 0: Black
|
||||
thumbBmp.write(black, 4);
|
||||
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; // Color 1: White
|
||||
thumbBmp.write(white, 4);
|
||||
|
||||
// Generate X pattern bitmap data
|
||||
// In BMP, 0 = black (first color in palette), 1 = white
|
||||
// We'll draw black pixels on white background
|
||||
for (int y = 0; y < height; y++) {
|
||||
std::vector<uint8_t> rowData(rowBytes, 0xFF); // Initialize to all white (1s)
|
||||
|
||||
// Map this row to a horizontal position for diagonals
|
||||
const int scaledY = (y * width) / height;
|
||||
const int thickness = 2; // thickness of diagonal lines in pixels
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
bool drawPixel = false;
|
||||
// Main diagonal (top-left to bottom-right)
|
||||
if (std::abs(x - scaledY) <= thickness) drawPixel = true;
|
||||
// Other diagonal (top-right to bottom-left)
|
||||
if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true;
|
||||
|
||||
if (drawPixel) {
|
||||
const int byteIndex = x / 8;
|
||||
const int bitIndex = 7 - (x % 8); // MSB first
|
||||
rowData[byteIndex] &= static_cast<uint8_t>(~(1 << bitIndex));
|
||||
}
|
||||
}
|
||||
|
||||
// Write the row data
|
||||
thumbBmp.write(rowData.data(), rowBytes);
|
||||
}
|
||||
|
||||
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();
|
||||
LOG_DBG("EBP", "Generated invalid format thumbnail BMP");
|
||||
return true;
|
||||
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 {
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
|
||||
bool Epub::generateInvalidFormatCoverBmp(bool cropped) const {
|
||||
// Create a simple 1-bit BMP with an X pattern to indicate invalid format.
|
||||
// This BMP is intentionally a valid image that visually indicates a
|
||||
// malformed/unsupported cover image instead of leaving an empty marker
|
||||
// file that would cause repeated generation attempts.
|
||||
// Derive logical portrait dimensions from the display hardware constants
|
||||
// EInkDisplay reports native panel orientation as 800x480; use min/max
|
||||
const int hwW = HalDisplay::DISPLAY_WIDTH;
|
||||
const int hwH = HalDisplay::DISPLAY_HEIGHT;
|
||||
const int width = std::min(hwW, hwH); // logical portrait width (480)
|
||||
const int height = std::max(hwW, hwH); // logical portrait height (800)
|
||||
const int rowBytes = ((width + 31) / 32) * 4; // 1-bit rows padded to 4-byte boundary
|
||||
const int imageSize = rowBytes * height;
|
||||
const int fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data
|
||||
const int dataOffset = 14 + 40 + 8;
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
// Write an empty bmp file to avoid generation attempts in the future
|
||||
FsFile thumbBmp;
|
||||
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||
thumbBmp.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// BMP file header (14 bytes)
|
||||
coverBmp.write('B');
|
||||
coverBmp.write('M');
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
// DIB header (BITMAPINFOHEADER - 40 bytes)
|
||||
uint32_t dibHeaderSize = 40;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t bmpWidth = width;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
|
||||
int32_t bmpHeight = -height; // Negative for top-down
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
|
||||
uint16_t planes = 1;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835; // 72 DPI
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
// Color palette (2 colors for 1-bit)
|
||||
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; // Color 0: Black
|
||||
coverBmp.write(black, 4);
|
||||
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; // Color 1: White
|
||||
coverBmp.write(white, 4);
|
||||
|
||||
// Generate X pattern bitmap data
|
||||
// In BMP, 0 = black (first color in palette), 1 = white
|
||||
// We'll draw black pixels on white background
|
||||
for (int y = 0; y < height; y++) {
|
||||
std::vector<uint8_t> rowData(rowBytes, 0xFF); // Initialize to all white (1s)
|
||||
|
||||
const int scaledY = (y * width) / height;
|
||||
const int thickness = 6; // thicker lines for full-cover visibility
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
bool drawPixel = false;
|
||||
if (std::abs(x - scaledY) <= thickness) drawPixel = true;
|
||||
if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true;
|
||||
|
||||
if (drawPixel) {
|
||||
const int byteIndex = x / 8;
|
||||
const int bitIndex = 7 - (x % 8);
|
||||
rowData[byteIndex] &= static_cast<uint8_t>(~(1 << bitIndex));
|
||||
}
|
||||
}
|
||||
|
||||
coverBmp.write(rowData.data(), rowBytes);
|
||||
}
|
||||
|
||||
coverBmp.close();
|
||||
LOG_DBG("EBP", "Generated invalid format cover BMP");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
if (itemHref.empty()) {
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
@@ -938,45 +822,3 @@ float Epub::calculateProgress(const int currentSpineIndex, const float currentSp
|
||||
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
|
||||
return totalProgress / static_cast<float>(bookSize);
|
||||
}
|
||||
|
||||
bool Epub::isValidThumbnailBmp(const std::string& bmpPath) {
|
||||
if (!Storage.exists(bmpPath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
FsFile file = Storage.open(bmpPath.c_str());
|
||||
if (!file) {
|
||||
LOG_ERR("EBP", "Failed to open thumbnail BMP at path: %s", bmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
if (fileSize == 0) {
|
||||
// Empty file is a marker for "no cover available"
|
||||
LOG_DBG("EBP", "Thumbnail BMP is empty (no cover marker) at path: %s", bmpPath.c_str());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
// BMP header starts with 'B' 'M'
|
||||
uint8_t header[2];
|
||||
size_t bytesRead = file.read(header, 2);
|
||||
if (bytesRead != 2) {
|
||||
LOG_ERR("EBP", "Failed to read thumbnail BMP header at path: %s", bmpPath.c_str());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "Thumbnail BMP header: %c%c", header[0], header[1]);
|
||||
file.close();
|
||||
return header[0] == 'B' && header[1] == 'M';
|
||||
}
|
||||
|
||||
std::vector<std::string> Epub::getCoverCandidates() const {
|
||||
std::vector<std::string> coverDirectories = {".", "images", "Images", "OEBPS", "OEBPS/images", "OEBPS/Images"};
|
||||
std::vector<std::string> coverExtensions = {".jpg", ".jpeg"}; // add ".png" when PNG cover support is implemented
|
||||
std::vector<std::string> coverCandidates;
|
||||
for (const auto& ext : coverExtensions) {
|
||||
for (const auto& dir : coverDirectories) {
|
||||
std::string candidate = (dir == ".") ? "cover" + ext : dir + "/cover" + ext;
|
||||
coverCandidates.push_back(candidate);
|
||||
}
|
||||
}
|
||||
return coverCandidates;
|
||||
}
|
||||
|
||||
@@ -52,23 +52,10 @@ class Epub {
|
||||
const std::string& getAuthor() const;
|
||||
const std::string& getLanguage() const;
|
||||
std::string getCoverBmpPath(bool cropped = false) const;
|
||||
// Generate a 1-bit BMP cover image from the EPUB cover image.
|
||||
// Returns true on success. On conversion failure, callers may use
|
||||
// `generateInvalidFormatCoverBmp` to create a valid marker BMP.
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
// Create a valid 1-bit BMP that visually indicates an invalid/unsupported
|
||||
// cover format (an X pattern). This prevents repeated generation attempts
|
||||
// by providing a valid BMP file that `isValidThumbnailBmp` accepts.
|
||||
bool generateInvalidFormatCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
// Generate a thumbnail BMP at the requested `height`. Returns true on
|
||||
// successful conversion. If conversion fails, `generateInvalidFormatThumbBmp`
|
||||
// can be used to write a valid marker image that prevents retries.
|
||||
bool generateThumbBmp(int height) const;
|
||||
// Create a valid 1-bit thumbnail BMP with an X marker indicating an
|
||||
// invalid/unsupported cover image instead of leaving an empty marker file.
|
||||
bool generateInvalidFormatThumbBmp(int height) const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
@@ -85,9 +72,4 @@ class Epub {
|
||||
size_t getBookSize() const;
|
||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
CssParser* getCssParser() const { return cssParser.get(); }
|
||||
|
||||
static bool isValidThumbnailBmp(const std::string& bmpPath);
|
||||
|
||||
private:
|
||||
std::vector<std::string> getCoverCandidates() const;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
// Cell padding in pixels (must match TABLE_CELL_PAD_* in ChapterHtmlSlimParser.cpp)
|
||||
static constexpr int TABLE_CELL_PADDING_X = 4;
|
||||
static constexpr int TABLE_CELL_PADDING_TOP = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageLine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
@@ -34,10 +25,6 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageImage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
// Images don't use fontId or text rendering
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
@@ -61,115 +48,6 @@ std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageTableRow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PageTableRow::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
const int baseX = xPos + xOffset;
|
||||
const int baseY = yPos + yOffset;
|
||||
|
||||
// Draw horizontal borders (top and bottom of this row)
|
||||
renderer.drawLine(baseX, baseY, baseX + totalWidth, baseY);
|
||||
renderer.drawLine(baseX, baseY + rowHeight, baseX + totalWidth, baseY + rowHeight);
|
||||
|
||||
// Draw vertical borders and render cell contents
|
||||
// Left edge
|
||||
renderer.drawLine(baseX, baseY, baseX, baseY + rowHeight);
|
||||
|
||||
for (const auto& cell : cells) {
|
||||
// Right vertical border for this cell
|
||||
const int cellRightX = baseX + cell.xOffset + cell.columnWidth;
|
||||
renderer.drawLine(cellRightX, baseY, cellRightX, baseY + rowHeight);
|
||||
|
||||
// Render each text line within the cell
|
||||
const int cellTextX = baseX + cell.xOffset + TABLE_CELL_PADDING_X;
|
||||
int cellLineY = baseY + 1 + TABLE_CELL_PADDING_TOP; // 1px border + top padding
|
||||
|
||||
for (const auto& line : cell.lines) {
|
||||
line->render(renderer, fontId, cellTextX, cellLineY);
|
||||
cellLineY += lineHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PageTableRow::serialize(FsFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
serialization::writePod(file, rowHeight);
|
||||
serialization::writePod(file, totalWidth);
|
||||
serialization::writePod(file, lineHeight);
|
||||
|
||||
const uint16_t cellCount = static_cast<uint16_t>(cells.size());
|
||||
serialization::writePod(file, cellCount);
|
||||
|
||||
for (const auto& cell : cells) {
|
||||
serialization::writePod(file, cell.xOffset);
|
||||
serialization::writePod(file, cell.columnWidth);
|
||||
|
||||
const uint16_t lineCount = static_cast<uint16_t>(cell.lines.size());
|
||||
serialization::writePod(file, lineCount);
|
||||
|
||||
for (const auto& line : cell.lines) {
|
||||
if (!line->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<PageTableRow> PageTableRow::deserialize(FsFile& file) {
|
||||
int16_t xPos, yPos, rowHeight, totalWidth, lineHeight;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
serialization::readPod(file, rowHeight);
|
||||
serialization::readPod(file, totalWidth);
|
||||
serialization::readPod(file, lineHeight);
|
||||
|
||||
uint16_t cellCount;
|
||||
serialization::readPod(file, cellCount);
|
||||
|
||||
// Sanity check
|
||||
if (cellCount > 100) {
|
||||
LOG_ERR("PTR", "Deserialization failed: cell count %u exceeds maximum", cellCount);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<PageTableCellData> cells;
|
||||
cells.resize(cellCount);
|
||||
|
||||
for (uint16_t c = 0; c < cellCount; ++c) {
|
||||
serialization::readPod(file, cells[c].xOffset);
|
||||
serialization::readPod(file, cells[c].columnWidth);
|
||||
|
||||
uint16_t lineCount;
|
||||
serialization::readPod(file, lineCount);
|
||||
|
||||
if (lineCount > 1000) {
|
||||
LOG_ERR("PTR", "Deserialization failed: line count %u in cell %u exceeds maximum", lineCount, c);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
cells[c].lines.reserve(lineCount);
|
||||
for (uint16_t l = 0; l < lineCount; ++l) {
|
||||
auto tb = TextBlock::deserialize(file);
|
||||
if (!tb) {
|
||||
return nullptr;
|
||||
}
|
||||
cells[c].lines.push_back(std::move(tb));
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<PageTableRow>(
|
||||
new PageTableRow(std::move(cells), rowHeight, totalWidth, lineHeight, xPos, yPos));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||
for (auto& element : elements) {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
@@ -181,7 +59,9 @@ bool Page::serialize(FsFile& file) const {
|
||||
serialization::writePod(file, count);
|
||||
|
||||
for (const auto& el : elements) {
|
||||
// Use getTag() method to determine type
|
||||
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||
|
||||
if (!el->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -203,13 +83,6 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
if (tag == TAG_PageLine) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else if (tag == TAG_PageTableRow) {
|
||||
auto tr = PageTableRow::deserialize(file);
|
||||
if (!tr) {
|
||||
LOG_ERR("PGE", "Deserialization failed for PageTableRow at element %u", i);
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(tr));
|
||||
} else if (tag == TAG_PageImage) {
|
||||
auto pi = PageImage::deserialize(file);
|
||||
page->elements.push_back(std::move(pi));
|
||||
@@ -221,50 +94,3 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
bool Page::getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const {
|
||||
bool firstImage = true;
|
||||
for (const auto& el : elements) {
|
||||
if (el->getTag() == TAG_PageImage) {
|
||||
PageImage* pi = static_cast<PageImage*>(el.get());
|
||||
ImageBlock* ib = pi->getImageBlock();
|
||||
|
||||
if (firstImage) {
|
||||
// Initialize with first image bounds
|
||||
outX = pi->xPos;
|
||||
outY = pi->yPos;
|
||||
outWidth = ib->getWidth();
|
||||
outHeight = ib->getHeight();
|
||||
firstImage = false;
|
||||
} else {
|
||||
// Expand bounding box to include this image
|
||||
int imgX = pi->xPos;
|
||||
int imgY = pi->yPos;
|
||||
int imgW = ib->getWidth();
|
||||
int imgH = ib->getHeight();
|
||||
|
||||
// Expand right boundary
|
||||
if (imgX + imgW > outX + outWidth) {
|
||||
outWidth = (imgX + imgW) - outX;
|
||||
}
|
||||
// Expand left boundary
|
||||
if (imgX < outX) {
|
||||
int oldRight = outX + outWidth;
|
||||
outX = imgX;
|
||||
outWidth = oldRight - outX;
|
||||
}
|
||||
// Expand bottom boundary
|
||||
if (imgY + imgH > outY + outHeight) {
|
||||
outHeight = (imgY + imgH) - outY;
|
||||
}
|
||||
// Expand top boundary
|
||||
if (imgY < outY) {
|
||||
int oldBottom = outY + outHeight;
|
||||
outY = imgY;
|
||||
outHeight = oldBottom - outY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !firstImage; // Return true if at least one image was found
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
|
||||
enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageTableRow = 2,
|
||||
TAG_PageImage = 3,
|
||||
TAG_PageImage = 2, // New tag
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
@@ -21,9 +20,9 @@ class PageElement {
|
||||
int16_t yPos;
|
||||
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||
virtual ~PageElement() = default;
|
||||
virtual PageElementTag getTag() const = 0;
|
||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||
virtual bool serialize(FsFile& file) = 0;
|
||||
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||
};
|
||||
|
||||
// a line from a block element
|
||||
@@ -33,44 +32,13 @@ class PageLine final : public PageElement {
|
||||
public:
|
||||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
/// Data for a single cell within a PageTableRow.
|
||||
struct PageTableCellData {
|
||||
std::vector<std::shared_ptr<TextBlock>> lines; // Laid-out text lines for this cell
|
||||
uint16_t columnWidth = 0; // Width of this column in pixels
|
||||
uint16_t xOffset = 0; // X offset of this cell within the row
|
||||
};
|
||||
|
||||
/// A table row element that renders cells in a column-aligned grid with borders.
|
||||
class PageTableRow final : public PageElement {
|
||||
std::vector<PageTableCellData> cells;
|
||||
int16_t rowHeight; // Total row height in pixels
|
||||
int16_t totalWidth; // Total table width in pixels
|
||||
int16_t lineHeight; // Height of one text line (for vertical positioning of cell lines)
|
||||
|
||||
public:
|
||||
PageTableRow(std::vector<PageTableCellData> cells, int16_t rowHeight, int16_t totalWidth, int16_t lineHeight,
|
||||
int16_t xPos, int16_t yPos)
|
||||
: PageElement(xPos, yPos),
|
||||
cells(std::move(cells)),
|
||||
rowHeight(rowHeight),
|
||||
totalWidth(totalWidth),
|
||||
lineHeight(lineHeight) {}
|
||||
|
||||
int16_t getHeight() const { return rowHeight; }
|
||||
PageElementTag getTag() const override { return TAG_PageTableRow; }
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
static std::unique_ptr<PageTableRow> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
// An image element on a page
|
||||
// New PageImage class
|
||||
class PageImage final : public PageElement {
|
||||
std::shared_ptr<ImageBlock> imageBlock;
|
||||
|
||||
@@ -81,9 +49,6 @@ class PageImage final : public PageElement {
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||
|
||||
// Helper to get image block dimensions (needed for bounding box calculation)
|
||||
ImageBlock* getImageBlock() const { return imageBlock.get(); }
|
||||
};
|
||||
|
||||
class Page {
|
||||
@@ -99,9 +64,4 @@ class Page {
|
||||
return std::any_of(elements.begin(), elements.end(),
|
||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
// Get the bounding box of all images on this page.
|
||||
// Returns true if page has images and fills out the bounding box coordinates.
|
||||
// If no images, returns false.
|
||||
bool getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
@@ -62,13 +63,6 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
}
|
||||
wordStyles.push_back(combinedStyle);
|
||||
wordContinues.push_back(attachToPrevious);
|
||||
forceBreakAfter.push_back(false);
|
||||
}
|
||||
|
||||
void ParsedText::addLineBreak() {
|
||||
if (!words.empty()) {
|
||||
forceBreakAfter.back() = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes data to minimize memory usage
|
||||
@@ -86,26 +80,37 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
// Build indexed continues vector from the parallel list for O(1) access during layout
|
||||
std::vector<bool> continuesVec(wordContinues.begin(), wordContinues.end());
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
if (hyphenationEnabled) {
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||
}
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine);
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, continuesVec, lineBreakIndices, processLine);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
||||
std::vector<uint16_t> wordWidths;
|
||||
wordWidths.reserve(words.size());
|
||||
const size_t totalWordCount = words.size();
|
||||
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, words[i], wordStyles[i]));
|
||||
std::vector<uint16_t> wordWidths;
|
||||
wordWidths.reserve(totalWordCount);
|
||||
|
||||
auto wordsIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
|
||||
while (wordsIt != words.end()) {
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
||||
|
||||
std::advance(wordsIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
}
|
||||
|
||||
return wordWidths;
|
||||
@@ -130,7 +135,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
// First word needs to fit in reduced width if there's an indent
|
||||
const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
while (wordWidths[i] > effectiveWidth) {
|
||||
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
|
||||
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true,
|
||||
&continuesVec)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -155,11 +161,6 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// If the previous word has a forced line break, this line cannot include word j
|
||||
if (j > static_cast<size_t>(i) && !forceBreakAfter.empty() && forceBreakAfter[j - 1]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
const int gap = j > static_cast<size_t>(i) && !continuesVec[j] ? spaceWidth : 0;
|
||||
currlen += wordWidths[j] + gap;
|
||||
@@ -168,11 +169,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
break;
|
||||
}
|
||||
|
||||
// Forced line break after word j overrides continuation (must end line here)
|
||||
const bool mustBreakHere = !forceBreakAfter.empty() && forceBreakAfter[j];
|
||||
|
||||
// Cannot break after word j if the next word attaches to it (unless forced)
|
||||
if (!mustBreakHere && j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||
// Cannot break after word j if the next word attaches to it (continuation group)
|
||||
if (j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -195,11 +193,6 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
dp[i] = cost;
|
||||
ans[i] = j; // j is the index of the last word in this optimal line
|
||||
}
|
||||
|
||||
// After evaluating cost, enforce forced break - no more words on this line
|
||||
if (mustBreakHere) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle oversized word: if no valid configuration found, force single-word line
|
||||
@@ -274,11 +267,6 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
|
||||
// Consume as many words as possible for current line, splitting when prefixes fit
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
// If the previous word has a forced line break, stop - this word starts a new line
|
||||
if (currentIndex > lineStart && !forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
const int spacing = isFirstWord || continuesVec[currentIndex] ? 0 : spaceWidth;
|
||||
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||
@@ -287,11 +275,6 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
if (lineWidth + candidateWidth <= effectivePageWidth) {
|
||||
lineWidth += candidateWidth;
|
||||
++currentIndex;
|
||||
|
||||
// If the word we just added has a forced break, end this line now
|
||||
if (!forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -299,8 +282,8 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
const int availableWidth = effectivePageWidth - lineWidth - spacing;
|
||||
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
||||
|
||||
if (availableWidth > 0 &&
|
||||
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
||||
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
||||
allowFallbackBreaks, &continuesVec)) {
|
||||
// Prefix now fits; append it to this line and move to next line
|
||||
lineWidth += spacing + wordWidths[currentIndex];
|
||||
++currentIndex;
|
||||
@@ -317,12 +300,7 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
|
||||
// Don't break before a continuation word (e.g., orphaned "?" after "question").
|
||||
// Backtrack to the start of the continuation group so the whole group moves to the next line.
|
||||
// But don't backtrack past a forced break point.
|
||||
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
|
||||
// Don't backtrack past a forced break
|
||||
if (!forceBreakAfter.empty() && forceBreakAfter[currentIndex - 1]) {
|
||||
break;
|
||||
}
|
||||
--currentIndex;
|
||||
}
|
||||
|
||||
@@ -337,14 +315,20 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
// available width.
|
||||
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
||||
const int fontId, std::vector<uint16_t>& wordWidths,
|
||||
const bool allowFallbackBreaks) {
|
||||
const bool allowFallbackBreaks, std::vector<bool>* continuesVec) {
|
||||
// Guard against invalid indices or zero available width before attempting to split.
|
||||
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& word = words[wordIndex];
|
||||
const auto style = wordStyles[wordIndex];
|
||||
// Get iterators to target word and style.
|
||||
auto wordIt = words.begin();
|
||||
auto styleIt = wordStyles.begin();
|
||||
std::advance(wordIt, wordIndex);
|
||||
std::advance(styleIt, wordIndex);
|
||||
|
||||
const std::string& word = *wordIt;
|
||||
const auto style = *styleIt;
|
||||
|
||||
// Collect candidate breakpoints (byte offsets and hyphen requirements).
|
||||
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
|
||||
@@ -381,26 +365,31 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
|
||||
// Split the word at the selected breakpoint and append a hyphen if required.
|
||||
std::string remainder = word.substr(chosenOffset);
|
||||
words[wordIndex].resize(chosenOffset);
|
||||
wordIt->resize(chosenOffset);
|
||||
if (chosenNeedsHyphen) {
|
||||
words[wordIndex].push_back('-');
|
||||
wordIt->push_back('-');
|
||||
}
|
||||
|
||||
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
|
||||
words.insert(words.begin() + wordIndex + 1, remainder);
|
||||
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
|
||||
auto insertWordIt = std::next(wordIt);
|
||||
auto insertStyleIt = std::next(styleIt);
|
||||
words.insert(insertWordIt, remainder);
|
||||
wordStyles.insert(insertStyleIt, style);
|
||||
|
||||
// The remainder inherits whatever continuation status the original word had with the word after it.
|
||||
const bool originalContinuedToNext = wordContinues[wordIndex];
|
||||
// Find the continues entry for the original word and insert the remainder's entry after it.
|
||||
auto continuesIt = wordContinues.begin();
|
||||
std::advance(continuesIt, wordIndex);
|
||||
const bool originalContinuedToNext = *continuesIt;
|
||||
// The original word (now prefix) does NOT continue to remainder (hyphen separates them)
|
||||
wordContinues[wordIndex] = false;
|
||||
wordContinues.insert(wordContinues.begin() + wordIndex + 1, originalContinuedToNext);
|
||||
*continuesIt = false;
|
||||
const auto insertContinuesIt = std::next(continuesIt);
|
||||
wordContinues.insert(insertContinuesIt, originalContinuedToNext);
|
||||
|
||||
// Forced break belongs to the original whole word; transfer it to the remainder (last part).
|
||||
if (!forceBreakAfter.empty()) {
|
||||
const bool originalForceBreak = forceBreakAfter[wordIndex];
|
||||
forceBreakAfter[wordIndex] = false; // prefix doesn't force break
|
||||
forceBreakAfter.insert(forceBreakAfter.begin() + wordIndex + 1, originalForceBreak);
|
||||
// Keep the indexed vector in sync if provided
|
||||
if (continuesVec) {
|
||||
(*continuesVec)[wordIndex] = false;
|
||||
continuesVec->insert(continuesVec->begin() + wordIndex + 1, originalContinuedToNext);
|
||||
}
|
||||
|
||||
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||
@@ -461,8 +450,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
// Continuation words attach to the previous word with no space before them
|
||||
std::vector<uint16_t> lineXPos;
|
||||
lineXPos.reserve(lineWordCount);
|
||||
std::list<uint16_t> lineXPos;
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
|
||||
@@ -475,10 +463,23 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing);
|
||||
}
|
||||
|
||||
// Build line data by moving from the original vectors using index range
|
||||
std::vector<std::string> lineWords(std::make_move_iterator(words.begin() + lastBreakAt),
|
||||
std::make_move_iterator(words.begin() + lineBreak));
|
||||
std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
|
||||
// Iterators always start at the beginning as we are moving content with splice below
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
auto wordContinuesEndIt = wordContinues.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
std::advance(wordContinuesEndIt, lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
|
||||
// Consume continues flags (not passed to TextBlock, but must be consumed to stay in sync)
|
||||
std::list<bool> lineContinues;
|
||||
lineContinues.splice(lineContinues.begin(), wordContinues, wordContinues.begin(), wordContinuesEndIt);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
if (containsSoftHyphen(word)) {
|
||||
@@ -489,22 +490,3 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
processLine(
|
||||
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||
}
|
||||
|
||||
uint16_t ParsedText::getNaturalWidth(const GfxRenderer& renderer, const int fontId) const {
|
||||
if (words.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
int totalWidth = 0;
|
||||
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
totalWidth += measureWordWidth(renderer, fontId, words[i], wordStyles[i]);
|
||||
// Add a space before this word unless it's the first word or a continuation
|
||||
if (i > 0 && !wordContinues[i]) {
|
||||
totalWidth += spaceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<uint16_t>(std::min(totalWidth, static_cast<int>(UINT16_MAX)));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -13,10 +14,9 @@
|
||||
class GfxRenderer;
|
||||
|
||||
class ParsedText {
|
||||
std::vector<std::string> words;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
std::vector<bool> forceBreakAfter; // true = mandatory line break after this word (e.g. <br> in table cells)
|
||||
std::list<std::string> words;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::list<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
@@ -28,7 +28,8 @@ class ParsedText {
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||
std::vector<bool>* continuesVec = nullptr);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
@@ -41,10 +42,6 @@ class ParsedText {
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||
|
||||
/// Mark a forced line break after the last word (e.g. for <br> within table cells).
|
||||
/// If no words have been added yet, this is a no-op.
|
||||
void addLineBreak();
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||
size_t size() const { return words.size(); }
|
||||
@@ -52,9 +49,4 @@ class ParsedText {
|
||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
bool includeLastLine = true);
|
||||
|
||||
/// Returns the "natural" width of the content if it were laid out on a single line
|
||||
/// (sum of word widths + space widths between non-continuation words).
|
||||
/// Used by table layout to determine column widths before line-breaking.
|
||||
uint16_t getNaturalWidth(const GfxRenderer& renderer, int fontId) const;
|
||||
};
|
||||
@@ -195,6 +195,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ParsedText.h"
|
||||
#include "css/CssStyle.h"
|
||||
|
||||
/// A single cell in a table row.
|
||||
struct TableCell {
|
||||
std::unique_ptr<ParsedText> content;
|
||||
bool isHeader = false; // true for <th>, false for <td>
|
||||
int colspan = 1; // number of logical columns this cell spans
|
||||
CssLength widthHint; // width hint from HTML attribute or CSS (if hasWidthHint)
|
||||
bool hasWidthHint = false;
|
||||
};
|
||||
|
||||
/// A single row in a table.
|
||||
struct TableRow {
|
||||
std::vector<TableCell> cells;
|
||||
};
|
||||
|
||||
/// Buffered table data collected during SAX parsing.
|
||||
/// The entire table must be buffered before layout because column widths
|
||||
/// depend on content across all rows.
|
||||
struct TableData {
|
||||
std::vector<TableRow> rows;
|
||||
std::vector<CssLength> colWidthHints; // width hints from <col> tags, indexed by logical column
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
@@ -47,8 +46,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
Serial.printf("[%lu] [IMG] Cache dimension mismatch: %dx%d vs %dx%d\n", millis(), cachedWidth, cachedHeight,
|
||||
expectedWidth, expectedHeight);
|
||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||
expectedHeight);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -57,20 +56,20 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
expectedWidth = cachedWidth;
|
||||
expectedHeight = cachedHeight;
|
||||
|
||||
Serial.printf("[%lu] [IMG] Loading from cache: %s (%dx%d)\n", millis(), cachePath.c_str(), cachedWidth, 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) {
|
||||
Serial.printf("[%lu] [IMG] Failed to allocate row buffer\n", millis());
|
||||
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) {
|
||||
Serial.printf("[%lu] [IMG] Cache read error at row %d\n", millis(), row);
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
@@ -88,22 +87,22 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
Serial.printf("[%lu] [IMG] Cache render complete\n", millis());
|
||||
LOG_DBG("IMG", "Cache render complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
Serial.printf("[%lu] [IMG] Rendering image at %d,%d: %s (%dx%d)\n", millis(), x, y, imagePath.c_str(), width, height);
|
||||
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) {
|
||||
Serial.printf("[%lu] [IMG] Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)\n", millis(), x, y, width,
|
||||
height, screenWidth, screenHeight);
|
||||
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||
screenHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,18 +116,18 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
// Check if image file exists
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [IMG] Image file not found: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
file.close();
|
||||
|
||||
if (fileSize == 0) {
|
||||
Serial.printf("[%lu] [IMG] Image file is empty: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Decoding and caching: %s\n", millis(), imagePath.c_str());
|
||||
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||
|
||||
RenderConfig config;
|
||||
config.x = x;
|
||||
@@ -143,19 +142,19 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
Serial.printf("[%lu] [IMG] No decoder found for image: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Using %s decoder\n", millis(), decoder->getFormatName());
|
||||
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [IMG] Failed to decode image: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Decode successful\n", millis());
|
||||
LOG_DBG("IMG", "Decode successful");
|
||||
}
|
||||
|
||||
bool ImageBlock::serialize(FsFile& file) {
|
||||
|
||||
@@ -12,13 +12,16 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
return;
|
||||
}
|
||||
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
const int wordX = wordXpos[i] + x;
|
||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
||||
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
||||
const int wordX = *wordXposIt + x;
|
||||
const EpdFontFamily::Style currentStyle = *wordStylesIt;
|
||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, currentStyle);
|
||||
|
||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = words[i];
|
||||
const std::string& w = *wordIt;
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||
@@ -30,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;
|
||||
@@ -38,6 +41,10 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
|
||||
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
||||
}
|
||||
|
||||
std::advance(wordIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
std::advance(wordXposIt, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,15 +80,15 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
|
||||
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
uint16_t wc;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
serialization::readPod(file, wc);
|
||||
|
||||
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
||||
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
||||
if (wc > 10000) {
|
||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||
return nullptr;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#include <EpdFontFamily.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
@@ -12,14 +12,14 @@
|
||||
// Represents a line of text on a page
|
||||
class TextBlock final : public Block {
|
||||
private:
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
|
||||
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||
std::list<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
@@ -27,9 +27,6 @@ class TextBlock final : public Block {
|
||||
~TextBlock() override = default;
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
const std::vector<std::string>& getWords() const { return words; }
|
||||
const std::vector<uint16_t>& getWordXpos() const { return wordXpos; }
|
||||
const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
// given a renderer works out where to break the words into lines
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "ImageDecoderFactory.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -35,7 +35,7 @@ ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& im
|
||||
return pngDecoder.get();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [DEC] No decoder found for image: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
|
||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||
if (width * height > MAX_SOURCE_PIXELS) {
|
||||
Serial.printf("[%lu] [IMG] Image too large (%dx%d = %d pixels %s), max supported: %d pixels\n", millis(), width,
|
||||
height, width * height, format.c_str(), MAX_SOURCE_PIXELS);
|
||||
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) {
|
||||
Serial.printf("[%lu] [IMG] Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.\n",
|
||||
millis(), feature.c_str(), imagePath.c_str());
|
||||
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||
imagePath.c_str());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
#include <picojpeg.h>
|
||||
@@ -23,7 +23,7 @@ struct JpegContext {
|
||||
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open file for dimensions: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,23 +34,23 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat
|
||||
file.close();
|
||||
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] Failed to init JPEG for dimensions: %d\n", millis(), status);
|
||||
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = imageInfo.m_width;
|
||||
out.height = imageInfo.m_height;
|
||||
Serial.printf("[%lu] [JPG] Image dimensions: %dx%d\n", millis(), out.width, out.height);
|
||||
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) {
|
||||
Serial.printf("[%lu] [JPG] Decoding JPEG: %s\n", millis(), imagePath.c_str());
|
||||
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open file: %s\n", millis(), imagePath.c_str());
|
||||
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] picojpeg init failed: %d\n", millis(), status);
|
||||
LOG_ERR("JPG", "picojpeg init failed: %d", status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@@ -93,12 +93,11 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
destHeight = (int)(imageInfo.m_height * scale);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [JPG] JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d\n", millis(),
|
||||
imageInfo.m_width, imageInfo.m_height, destWidth, destHeight, scale, imageInfo.m_scanType,
|
||||
imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
||||
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) {
|
||||
Serial.printf("[%lu] [JPG] Null buffer pointers in imageInfo\n", millis());
|
||||
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@@ -111,7 +110,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
bool caching = !config.cachePath.empty();
|
||||
if (caching) {
|
||||
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to allocate cache buffer, continuing without caching\n", millis());
|
||||
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||
caching = false;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +124,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
break;
|
||||
}
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] MCU decode failed: %d\n", millis(), status);
|
||||
LOG_ERR("JPG", "MCU decode failed: %d", status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@@ -254,7 +253,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [JPG] Decoding complete\n", millis());
|
||||
LOG_DBG("JPG", "Decoding complete");
|
||||
file.close();
|
||||
|
||||
// Write cache file if caching was enabled
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstring>
|
||||
@@ -32,14 +31,13 @@ struct PixelCache {
|
||||
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
size_t bufferSize = (size_t)bytesPerRow * h;
|
||||
if (bufferSize > MAX_CACHE_BYTES) {
|
||||
Serial.printf("[%lu] [IMG] Cache buffer too large: %d bytes for %dx%d (limit %d)\n", millis(), bufferSize, w, h,
|
||||
MAX_CACHE_BYTES);
|
||||
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);
|
||||
Serial.printf("[%lu] [IMG] Allocated cache buffer: %d bytes for %dx%d\n", millis(), bufferSize, w, h);
|
||||
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
|
||||
}
|
||||
return buffer != nullptr;
|
||||
}
|
||||
@@ -60,7 +58,7 @@ struct PixelCache {
|
||||
|
||||
FsFile cacheFile;
|
||||
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||
Serial.printf("[%lu] [IMG] Failed to open cache file for writing: %s\n", millis(), cachePath.c_str());
|
||||
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,8 +69,7 @@ struct PixelCache {
|
||||
cacheFile.write(buffer, bytesPerRow * height);
|
||||
cacheFile.close();
|
||||
|
||||
Serial.printf("[%lu] [IMG] Cache written: %s (%dx%d, %d bytes)\n", millis(), cachePath.c_str(), width, height,
|
||||
4 + bytesPerRow * height);
|
||||
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <PNGdec.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
@@ -216,14 +216,13 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
Serial.printf("[%lu] [PNG] Not enough heap for PNG decoder (%u free, need %u)\n", millis(), freeHeap,
|
||||
MIN_FREE_HEAP_FOR_PNG);
|
||||
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) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate PNG decoder for dimensions\n", millis());
|
||||
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -231,7 +230,7 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
|
||||
nullptr);
|
||||
|
||||
if (rc != 0) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open PNG for dimensions: %d\n", millis(), rc);
|
||||
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
@@ -246,19 +245,18 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
|
||||
|
||||
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
Serial.printf("[%lu] [PNG] Decoding PNG: %s\n", millis(), imagePath.c_str());
|
||||
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
|
||||
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
Serial.printf("[%lu] [PNG] Not enough heap for PNG decoder (%u free, need %u)\n", millis(), freeHeap,
|
||||
MIN_FREE_HEAP_FOR_PNG);
|
||||
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) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate PNG decoder\n", millis());
|
||||
LOG_ERR("PNG", "Failed to allocate PNG decoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -271,7 +269,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
pngDrawCallback);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open PNG: %d\n", millis(), rc);
|
||||
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
@@ -303,8 +301,8 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
Serial.printf("[%lu] [PNG] PNG %dx%d -> %dx%d (scale %.2f), bpp: %d\n", millis(), ctx.srcWidth, ctx.srcHeight,
|
||||
ctx.dstWidth, ctx.dstHeight, ctx.scale, png->getBpp());
|
||||
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);
|
||||
@@ -314,7 +312,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate gray line buffer\n", millis());
|
||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
@@ -324,7 +322,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate cache buffer, continuing without caching\n", millis());
|
||||
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
@@ -337,7 +335,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("[%lu] [PNG] Decode failed: %d\n", millis(), rc);
|
||||
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||
png->close();
|
||||
delete png;
|
||||
return false;
|
||||
@@ -345,7 +343,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
|
||||
png->close();
|
||||
delete png;
|
||||
Serial.printf("[%lu] [PNG] PNG decoding complete - render time: %lu ms\n", millis(), decodeTime);
|
||||
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) {
|
||||
|
||||
@@ -295,9 +295,6 @@ void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& sty
|
||||
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
|
||||
1;
|
||||
}
|
||||
} else if (propNameBuf == "width") {
|
||||
style.width = interpretLength(propValueBuf);
|
||||
style.defined.width = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ struct CssPropertyFlags {
|
||||
uint16_t paddingBottom : 1;
|
||||
uint16_t paddingLeft : 1;
|
||||
uint16_t paddingRight : 1;
|
||||
uint16_t width : 1;
|
||||
|
||||
CssPropertyFlags()
|
||||
: textAlign(0),
|
||||
@@ -84,19 +83,17 @@ struct CssPropertyFlags {
|
||||
paddingTop(0),
|
||||
paddingBottom(0),
|
||||
paddingLeft(0),
|
||||
paddingRight(0),
|
||||
width(0) {}
|
||||
paddingRight(0) {}
|
||||
|
||||
[[nodiscard]] bool anySet() const {
|
||||
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width;
|
||||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight;
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||
width = 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -118,7 +115,6 @@ struct CssStyle {
|
||||
CssLength paddingBottom; // Padding after
|
||||
CssLength paddingLeft; // Padding left
|
||||
CssLength paddingRight; // Padding right
|
||||
CssLength width; // Element width (used for table columns/cells)
|
||||
|
||||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||
|
||||
@@ -177,10 +173,6 @@ struct CssStyle {
|
||||
paddingRight = base.paddingRight;
|
||||
defined.paddingRight = 1;
|
||||
}
|
||||
if (base.hasWidth()) {
|
||||
width = base.width;
|
||||
defined.width = 1;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
||||
@@ -196,7 +188,6 @@ struct CssStyle {
|
||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||||
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||||
[[nodiscard]] bool hasWidth() const { return defined.width; }
|
||||
|
||||
void reset() {
|
||||
textAlign = CssTextAlign::Left;
|
||||
@@ -206,7 +197,6 @@ struct CssStyle {
|
||||
textIndent = CssLength{};
|
||||
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||
width = CssLength{};
|
||||
defined.clearAll();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,84 +1,48 @@
|
||||
#include "LanguageRegistry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
#include "HyphenationCommon.h"
|
||||
#ifndef OMIT_HYPH_DE
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_EN
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_ES
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_FR
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_IT
|
||||
#include "generated/hyph-it.trie.h"
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_RU
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#ifndef OMIT_HYPH_EN
|
||||
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_FR
|
||||
LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_DE
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_RU
|
||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_ES
|
||||
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_IT
|
||||
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||
#endif
|
||||
|
||||
const LanguageEntryView entries() {
|
||||
static const std::vector<LanguageEntry> kEntries = {
|
||||
#ifndef OMIT_HYPH_EN
|
||||
{"english", "en", &englishHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_FR
|
||||
using EntryArray = std::array<LanguageEntry, 6>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_DE
|
||||
{"german", "de", &germanHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_RU
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_ES
|
||||
{"spanish", "es", &spanishHyphenator},
|
||||
#endif
|
||||
#ifndef OMIT_HYPH_IT
|
||||
{"italian", "it", &italianHyphenator},
|
||||
#endif
|
||||
};
|
||||
static const LanguageEntryView view{kEntries.data(), kEntries.size()};
|
||||
return view;
|
||||
{"italian", "it", &italianHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) {
|
||||
const auto allEntries = entries();
|
||||
const auto& allEntries = entries();
|
||||
const auto it = std::find_if(allEntries.begin(), allEntries.end(),
|
||||
[&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; });
|
||||
return (it != allEntries.end()) ? it->hyphenator : nullptr;
|
||||
}
|
||||
|
||||
LanguageEntryView getLanguageEntries() {
|
||||
return entries();
|
||||
const auto& allEntries = entries();
|
||||
return LanguageEntryView{allEntries.data(), allEntries.size()};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -7,107 +7,107 @@
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||
0x00, 0x00, 0x05, 0xC4, 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,
|
||||
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, 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, 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, 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,
|
||||
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
@@ -6,8 +6,6 @@
|
||||
#include <Logging.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "../../Epub.h"
|
||||
#include "../Page.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
@@ -38,30 +36,8 @@ constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
const char* SKIP_TAGS[] = {"head"};
|
||||
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
|
||||
|
||||
// Table tags that are transparent containers (just depth tracking, no special handling)
|
||||
const char* TABLE_TRANSPARENT_TAGS[] = {"thead", "tbody", "tfoot", "colgroup"};
|
||||
constexpr int NUM_TABLE_TRANSPARENT_TAGS = sizeof(TABLE_TRANSPARENT_TAGS) / sizeof(TABLE_TRANSPARENT_TAGS[0]);
|
||||
|
||||
// Table tags to skip entirely (their children produce no useful output)
|
||||
const char* TABLE_SKIP_TAGS[] = {"caption"};
|
||||
constexpr int NUM_TABLE_SKIP_TAGS = sizeof(TABLE_SKIP_TAGS) / sizeof(TABLE_SKIP_TAGS[0]);
|
||||
|
||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||
|
||||
// Parse an HTML width attribute value into a CssLength.
|
||||
// "200" -> 200px, "50%" -> 50 percent. Returns false if the value can't be parsed.
|
||||
static bool parseHtmlWidthAttr(const char* value, CssLength& out) {
|
||||
char* end = nullptr;
|
||||
const float num = strtof(value, &end);
|
||||
if (end == value || num < 0) return false;
|
||||
if (*end == '%') {
|
||||
out = CssLength(num, CssUnit::Percent);
|
||||
} else {
|
||||
out = CssLength(num, CssUnit::Pixels);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// given the start and end of a tag, check to see if it matches a known tag
|
||||
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
|
||||
for (int i = 0; i < possible_tag_count; i++) {
|
||||
@@ -119,37 +95,13 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
|
||||
// flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
|
||||
// Handle double-encoded entities (e.g. &nbsp; in source -> literal " " after
|
||||
// XML parsing). Common in Wikipedia and other generated EPUBs. Replace with a space so the text
|
||||
// renders cleanly. The space stays within the word, preserving non-breaking behavior.
|
||||
std::string flushedWord(partWordBuffer);
|
||||
size_t entityPos = 0;
|
||||
while ((entityPos = flushedWord.find(" ", entityPos)) != std::string::npos) {
|
||||
flushedWord.replace(entityPos, 6, " ");
|
||||
entityPos += 1;
|
||||
}
|
||||
|
||||
currentTextBlock->addWord(flushedWord, fontStyle, false, nextWordContinues);
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
// When inside a table cell, don't lay out to the page -- insert a forced line break
|
||||
// within the cell's ParsedText so that block elements (p, div, br) create visual breaks.
|
||||
if (inTable) {
|
||||
if (partWordBufferIndex > 0) {
|
||||
flushPartWordBuffer();
|
||||
}
|
||||
if (currentTextBlock && !currentTextBlock->isEmpty()) {
|
||||
currentTextBlock->addLineBreak();
|
||||
}
|
||||
nextWordContinues = false;
|
||||
return;
|
||||
}
|
||||
|
||||
nextWordContinues = false; // New block = new paragraph, no continuation
|
||||
if (currentTextBlock) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
@@ -192,184 +144,21 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
centeredBlockStyle.textAlignDefined = true;
|
||||
centeredBlockStyle.alignment = CssTextAlign::Center;
|
||||
|
||||
// --- Table handling ---
|
||||
// Special handling for tables - show placeholder text instead of dropping silently
|
||||
if (strcmp(name, "table") == 0) {
|
||||
if (self->inTable) {
|
||||
// Nested table: skip it entirely for v1
|
||||
self->skipUntilDepth = self->depth;
|
||||
// Add placeholder text
|
||||
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, "[Table omitted]", strlen("[Table omitted]"));
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush any pending content before the table
|
||||
if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) {
|
||||
self->makePages();
|
||||
}
|
||||
|
||||
self->inTable = true;
|
||||
self->tableData.reset(new TableData());
|
||||
|
||||
// Create a safe empty currentTextBlock so character data outside cells
|
||||
// (e.g. whitespace between tags) doesn't crash
|
||||
auto tableBlockStyle = BlockStyle();
|
||||
tableBlockStyle.alignment = CssTextAlign::Left;
|
||||
self->currentTextBlock.reset(new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, tableBlockStyle));
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Table structure tags (only when inside a table)
|
||||
if (self->inTable) {
|
||||
if (strcmp(name, "tr") == 0) {
|
||||
self->tableData->rows.push_back(TableRow());
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// <col> — capture width hint for column sizing
|
||||
if (strcmp(name, "col") == 0) {
|
||||
CssLength widthHint;
|
||||
bool hasHint = false;
|
||||
|
||||
// Parse HTML width attribute
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "width") == 0) {
|
||||
hasHint = parseHtmlWidthAttr(atts[i + 1], widthHint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSS width (inline style) overrides HTML attribute
|
||||
if (self->cssParser) {
|
||||
std::string styleAttr;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "style") == 0) {
|
||||
styleAttr = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!styleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||
if (inlineStyle.hasWidth()) {
|
||||
widthHint = inlineStyle.width;
|
||||
hasHint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHint) {
|
||||
self->tableData->colWidthHints.push_back(widthHint);
|
||||
} else {
|
||||
// Push a zero-value placeholder to maintain index alignment
|
||||
self->tableData->colWidthHints.push_back(CssLength());
|
||||
}
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) {
|
||||
const bool isHeader = strcmp(name, "th") == 0;
|
||||
|
||||
// Parse colspan and width attributes
|
||||
int colspan = 1;
|
||||
CssLength cellWidthHint;
|
||||
bool hasCellWidthHint = false;
|
||||
std::string cellStyleAttr;
|
||||
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "colspan") == 0) {
|
||||
colspan = atoi(atts[i + 1]);
|
||||
if (colspan < 1) colspan = 1;
|
||||
} else if (strcmp(atts[i], "width") == 0) {
|
||||
hasCellWidthHint = parseHtmlWidthAttr(atts[i + 1], cellWidthHint);
|
||||
} else if (strcmp(atts[i], "style") == 0) {
|
||||
cellStyleAttr = atts[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSS width (inline style or stylesheet) overrides HTML attribute
|
||||
if (self->cssParser) {
|
||||
std::string classAttr;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "class") == 0) {
|
||||
classAttr = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
CssStyle cellCssStyle = self->cssParser->resolveStyle(name, classAttr);
|
||||
if (!cellStyleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(cellStyleAttr);
|
||||
cellCssStyle.applyOver(inlineStyle);
|
||||
}
|
||||
if (cellCssStyle.hasWidth()) {
|
||||
cellWidthHint = cellCssStyle.width;
|
||||
hasCellWidthHint = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there's a row to add cells to
|
||||
if (self->tableData->rows.empty()) {
|
||||
self->tableData->rows.push_back(TableRow());
|
||||
}
|
||||
|
||||
// Create a new ParsedText for this cell (characterData will flow into it)
|
||||
auto cellBlockStyle = BlockStyle();
|
||||
cellBlockStyle.alignment = CssTextAlign::Left;
|
||||
cellBlockStyle.textAlignDefined = true;
|
||||
// Explicitly disable paragraph indent for table cells
|
||||
cellBlockStyle.textIndent = 0;
|
||||
cellBlockStyle.textIndentDefined = true;
|
||||
self->currentTextBlock.reset(
|
||||
new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, cellBlockStyle));
|
||||
self->nextWordContinues = false;
|
||||
|
||||
// Track the cell
|
||||
auto& currentRow = self->tableData->rows.back();
|
||||
currentRow.cells.push_back(TableCell());
|
||||
currentRow.cells.back().isHeader = isHeader;
|
||||
currentRow.cells.back().colspan = colspan;
|
||||
if (hasCellWidthHint) {
|
||||
currentRow.cells.back().widthHint = cellWidthHint;
|
||||
currentRow.cells.back().hasWidthHint = true;
|
||||
}
|
||||
|
||||
// Apply bold for header cells
|
||||
if (isHeader) {
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Transparent table container tags
|
||||
if (matches(name, TABLE_TRANSPARENT_TAGS, NUM_TABLE_TRANSPARENT_TAGS)) {
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip colgroup, col, caption
|
||||
if (matches(name, TABLE_SKIP_TAGS, NUM_TABLE_SKIP_TAGS)) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Other tags inside table cells (p, div, span, b, i, etc.) fall through
|
||||
// to the normal handling below. startNewTextBlock is a no-op when inTable.
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
std::string src;
|
||||
std::string alt;
|
||||
@@ -718,8 +507,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
||||
// memory.
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
// Skip this when inside a table - cell content is buffered for later layout.
|
||||
if (!self->inTable && self->currentTextBlock->size() > 750) {
|
||||
if (self->currentTextBlock->size() > 750) {
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
self->currentTextBlock->layoutAndExtractLines(
|
||||
self->renderer, self->fontId, self->viewportWidth,
|
||||
@@ -757,17 +545,15 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
|
||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||
const bool isTableCellTag = strcmp(name, "td") == 0 || strcmp(name, "th") == 0;
|
||||
const bool isTableTag = strcmp(name, "table") == 0;
|
||||
|
||||
// Flush buffer with current style BEFORE any style changes
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// Flush if style will change OR if we're closing a block/structural element
|
||||
const bool isInlineTag = !headerOrBlockTag && !isTableTag && !isTableCellTag &&
|
||||
const bool isInlineTag = !headerOrBlockTag && strcmp(name, "table") != 0 &&
|
||||
!matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
|
||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || isTableTag || isTableCellTag ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 ||
|
||||
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||
|
||||
if (shouldFlush) {
|
||||
@@ -779,57 +565,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
}
|
||||
}
|
||||
|
||||
// --- Table cell/row/table close handling ---
|
||||
if (self->inTable) {
|
||||
if (isTableCellTag) {
|
||||
// Save the current cell content into the table data
|
||||
if (self->tableData && !self->tableData->rows.empty()) {
|
||||
auto& currentRow = self->tableData->rows.back();
|
||||
if (!currentRow.cells.empty()) {
|
||||
currentRow.cells.back().content = std::move(self->currentTextBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a safe empty ParsedText so character data between cells doesn't crash
|
||||
auto safeBlockStyle = BlockStyle();
|
||||
safeBlockStyle.alignment = CssTextAlign::Left;
|
||||
self->currentTextBlock.reset(
|
||||
new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, safeBlockStyle));
|
||||
self->nextWordContinues = false;
|
||||
}
|
||||
|
||||
if (isTableTag) {
|
||||
// Process the entire buffered table
|
||||
self->depth -= 1;
|
||||
|
||||
// Clean up style state for this depth
|
||||
if (self->skipUntilDepth == self->depth) self->skipUntilDepth = INT_MAX;
|
||||
if (self->boldUntilDepth == self->depth) self->boldUntilDepth = INT_MAX;
|
||||
if (self->italicUntilDepth == self->depth) self->italicUntilDepth = INT_MAX;
|
||||
if (self->underlineUntilDepth == self->depth) self->underlineUntilDepth = INT_MAX;
|
||||
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
||||
self->inlineStyleStack.pop_back();
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
|
||||
self->processTable();
|
||||
|
||||
self->inTable = false;
|
||||
self->tableData.reset();
|
||||
|
||||
// Restore a fresh text block for content after the table
|
||||
auto paragraphAlignmentBlockStyle = BlockStyle();
|
||||
paragraphAlignmentBlockStyle.textAlignDefined = true;
|
||||
const auto align = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
|
||||
? CssTextAlign::Justify
|
||||
: static_cast<CssTextAlign>(self->paragraphAlignment);
|
||||
paragraphAlignmentBlockStyle.alignment = align;
|
||||
self->currentTextBlock.reset(
|
||||
new ParsedText(self->extraParagraphSpacing, self->hyphenationEnabled, paragraphAlignmentBlockStyle));
|
||||
return; // depth already decremented, skip the normal endElement cleanup
|
||||
}
|
||||
}
|
||||
|
||||
self->depth -= 1;
|
||||
|
||||
// Leaving skip
|
||||
@@ -1017,335 +752,3 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
currentPageNextY += lineHeight / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table processing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Cell padding in pixels (horizontal space between grid line and cell text)
|
||||
static constexpr int TABLE_CELL_PAD_X = 4;
|
||||
// Vertical cell padding — asymmetric because font metrics include internal leading (whitespace
|
||||
// above glyphs), so the top already has built-in visual space. Less explicit padding on top,
|
||||
// more on bottom, produces visually balanced results.
|
||||
static constexpr int TABLE_CELL_PAD_TOP = 1;
|
||||
static constexpr int TABLE_CELL_PAD_BOTTOM = 3;
|
||||
// Minimum usable column width in pixels (below this text is unreadable)
|
||||
static constexpr int TABLE_MIN_COL_WIDTH = 30;
|
||||
// Grid line width in pixels
|
||||
static constexpr int TABLE_GRID_LINE_PX = 1;
|
||||
|
||||
void ChapterHtmlSlimParser::addTableRowToPage(std::shared_ptr<PageTableRow> row) {
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
const int16_t rowH = row->getHeight();
|
||||
|
||||
// If this row doesn't fit on the current page, start a new one
|
||||
if (currentPageNextY + rowH > viewportHeight) {
|
||||
completePageFn(std::move(currentPage));
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
row->xPos = 0;
|
||||
row->yPos = currentPageNextY;
|
||||
currentPage->elements.push_back(std::move(row));
|
||||
currentPageNextY += rowH;
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::processTable() {
|
||||
if (!tableData || tableData->rows.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
const int lh = static_cast<int>(renderer.getLineHeight(fontId) * lineCompression);
|
||||
|
||||
// 1. Determine logical column count using colspan.
|
||||
// Each cell occupies cell.colspan logical columns. The total for a row is the sum of colspans.
|
||||
size_t numCols = 0;
|
||||
for (const auto& row : tableData->rows) {
|
||||
size_t rowLogicalCols = 0;
|
||||
for (const auto& cell : row.cells) {
|
||||
rowLogicalCols += static_cast<size_t>(cell.colspan);
|
||||
}
|
||||
numCols = std::max(numCols, rowLogicalCols);
|
||||
}
|
||||
|
||||
if (numCols == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Measure natural width of each cell and compute per-column max natural width.
|
||||
// Only non-spanning cells (colspan==1) contribute to individual column widths.
|
||||
// Spanning cells use the combined width of their spanned columns.
|
||||
std::vector<uint16_t> colNaturalWidth(numCols, 0);
|
||||
|
||||
for (const auto& row : tableData->rows) {
|
||||
size_t logicalCol = 0;
|
||||
for (const auto& cell : row.cells) {
|
||||
if (cell.colspan == 1 && cell.content && !cell.content->isEmpty()) {
|
||||
if (logicalCol < numCols) {
|
||||
const uint16_t w = cell.content->getNaturalWidth(renderer, fontId);
|
||||
if (w > colNaturalWidth[logicalCol]) {
|
||||
colNaturalWidth[logicalCol] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
logicalCol += static_cast<size_t>(cell.colspan);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calculate column widths to fit viewport.
|
||||
// Available width = viewport - outer borders - internal column borders - cell padding
|
||||
const int totalGridLines = static_cast<int>(numCols) + 1; // left + between columns + right
|
||||
const int totalPadding = static_cast<int>(numCols) * TABLE_CELL_PAD_X * 2;
|
||||
const int availableForContent = viewportWidth - totalGridLines * TABLE_GRID_LINE_PX - totalPadding;
|
||||
|
||||
// 3a. Resolve width hints per column.
|
||||
// Priority: <col> hints > max cell hint (colspan=1 only).
|
||||
// Percentages are relative to availableForContent.
|
||||
const float emSize = static_cast<float>(lh);
|
||||
const float containerW = static_cast<float>(std::max(availableForContent, 0));
|
||||
|
||||
std::vector<int> colHintedWidth(numCols, -1); // -1 = no hint
|
||||
|
||||
// From <col> tags
|
||||
for (size_t c = 0; c < numCols && c < tableData->colWidthHints.size(); ++c) {
|
||||
const auto& hint = tableData->colWidthHints[c];
|
||||
if (hint.value > 0) {
|
||||
int px = static_cast<int>(hint.toPixels(emSize, containerW));
|
||||
if (px > 0) {
|
||||
colHintedWidth[c] = std::max(px, TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From <td>/<th> cell width hints (only override if no <col> hint exists for this column)
|
||||
for (const auto& row : tableData->rows) {
|
||||
size_t logicalCol = 0;
|
||||
for (const auto& cell : row.cells) {
|
||||
if (cell.colspan == 1 && cell.hasWidthHint && logicalCol < numCols) {
|
||||
if (colHintedWidth[logicalCol] < 0) { // no <col> hint yet
|
||||
int px = static_cast<int>(cell.widthHint.toPixels(emSize, containerW));
|
||||
if (px > colHintedWidth[logicalCol]) {
|
||||
colHintedWidth[logicalCol] = std::max(px, TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
logicalCol += static_cast<size_t>(cell.colspan);
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Distribute column widths: hinted columns get their hint, unhinted use auto-sizing.
|
||||
std::vector<uint16_t> colWidths(numCols, 0);
|
||||
|
||||
if (availableForContent <= 0) {
|
||||
const uint16_t equalWidth = static_cast<uint16_t>(viewportWidth / numCols);
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
colWidths[c] = equalWidth;
|
||||
}
|
||||
} else {
|
||||
// First, assign hinted columns and track how much space they consume
|
||||
int hintedSpaceUsed = 0;
|
||||
size_t unhintedCount = 0;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
hintedSpaceUsed += colHintedWidth[c];
|
||||
} else {
|
||||
unhintedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If hinted columns exceed available space, scale them down proportionally
|
||||
if (hintedSpaceUsed > availableForContent && hintedSpaceUsed > 0) {
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
colHintedWidth[c] = colHintedWidth[c] * availableForContent / hintedSpaceUsed;
|
||||
colHintedWidth[c] = std::max(colHintedWidth[c], TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
// Recalculate
|
||||
hintedSpaceUsed = 0;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
hintedSpaceUsed += colHintedWidth[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign hinted columns
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] > 0) {
|
||||
colWidths[c] = static_cast<uint16_t>(colHintedWidth[c]);
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute remaining space among unhinted columns using the existing algorithm
|
||||
const int remainingForUnhinted = std::max(availableForContent - hintedSpaceUsed, 0);
|
||||
|
||||
if (unhintedCount > 0 && remainingForUnhinted > 0) {
|
||||
// Compute total natural width of unhinted columns
|
||||
int totalNaturalUnhinted = 0;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
totalNaturalUnhinted += colNaturalWidth[c];
|
||||
}
|
||||
}
|
||||
|
||||
if (totalNaturalUnhinted <= remainingForUnhinted) {
|
||||
// All unhinted content fits — distribute extra space equally among unhinted columns
|
||||
const int extraSpace = remainingForUnhinted - totalNaturalUnhinted;
|
||||
const int perColExtra = extraSpace / static_cast<int>(unhintedCount);
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
colWidths[c] = static_cast<uint16_t>(colNaturalWidth[c] + perColExtra);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unhinted content exceeds remaining space — two-pass fair-share among unhinted columns
|
||||
const int equalShare = remainingForUnhinted / static_cast<int>(unhintedCount);
|
||||
|
||||
int spaceUsedByFitting = 0;
|
||||
int naturalOfWide = 0;
|
||||
size_t wideCount = 0;
|
||||
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
if (static_cast<int>(colNaturalWidth[c]) <= equalShare) {
|
||||
colWidths[c] = colNaturalWidth[c];
|
||||
spaceUsedByFitting += colNaturalWidth[c];
|
||||
} else {
|
||||
naturalOfWide += colNaturalWidth[c];
|
||||
wideCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int wideSpace = remainingForUnhinted - spaceUsedByFitting;
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0 && static_cast<int>(colNaturalWidth[c]) > equalShare) {
|
||||
if (naturalOfWide > 0 && wideCount > 1) {
|
||||
int proportional = static_cast<int>(colNaturalWidth[c]) * wideSpace / naturalOfWide;
|
||||
colWidths[c] = static_cast<uint16_t>(std::max(proportional, TABLE_MIN_COL_WIDTH));
|
||||
} else {
|
||||
colWidths[c] = static_cast<uint16_t>(std::max(wideSpace, TABLE_MIN_COL_WIDTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (unhintedCount > 0) {
|
||||
// No remaining space for unhinted columns — give them minimum width
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
if (colHintedWidth[c] <= 0) {
|
||||
colWidths[c] = static_cast<uint16_t>(TABLE_MIN_COL_WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute column x-offsets (cumulative: border + padding + content width + padding + border ...)
|
||||
std::vector<uint16_t> colXOffsets(numCols, 0);
|
||||
int xAccum = TABLE_GRID_LINE_PX; // start after left border
|
||||
for (size_t c = 0; c < numCols; ++c) {
|
||||
colXOffsets[c] = static_cast<uint16_t>(xAccum);
|
||||
xAccum += TABLE_CELL_PAD_X + colWidths[c] + TABLE_CELL_PAD_X + TABLE_GRID_LINE_PX;
|
||||
}
|
||||
const int16_t totalTableWidth = static_cast<int16_t>(xAccum);
|
||||
|
||||
// Helper: compute the combined content width for a cell spanning multiple columns.
|
||||
// This includes the content widths plus the internal grid lines and padding between spanned columns.
|
||||
auto spanContentWidth = [&](size_t startCol, int colspan) -> uint16_t {
|
||||
int width = 0;
|
||||
for (int s = 0; s < colspan && startCol + s < numCols; ++s) {
|
||||
width += colWidths[startCol + s];
|
||||
if (s > 0) {
|
||||
// Add internal padding and grid line between spanned columns
|
||||
width += TABLE_CELL_PAD_X * 2 + TABLE_GRID_LINE_PX;
|
||||
}
|
||||
}
|
||||
return static_cast<uint16_t>(std::max(width, 0));
|
||||
};
|
||||
|
||||
// Helper: compute the full cell width (including padding on both sides) for a spanning cell.
|
||||
auto spanFullCellWidth = [&](size_t startCol, int colspan) -> uint16_t {
|
||||
if (colspan <= 0 || startCol >= numCols) return 0;
|
||||
const size_t endCol = std::min(startCol + static_cast<size_t>(colspan), numCols) - 1;
|
||||
// From the left edge of startCol's cell to the right edge of endCol's cell
|
||||
const int leftEdge = colXOffsets[startCol];
|
||||
const int rightEdge = colXOffsets[endCol] + TABLE_CELL_PAD_X + colWidths[endCol] + TABLE_CELL_PAD_X;
|
||||
return static_cast<uint16_t>(rightEdge - leftEdge);
|
||||
};
|
||||
|
||||
// 4. Lay out each row: map cells to logical columns, create PageTableRow
|
||||
for (auto& row : tableData->rows) {
|
||||
// Build cell data for this row, one entry per CELL (not per logical column).
|
||||
// Each PageTableCellData gets the correct x-offset and combined column width.
|
||||
std::vector<PageTableCellData> cellDataVec;
|
||||
size_t maxLinesInRow = 1;
|
||||
size_t logicalCol = 0;
|
||||
|
||||
for (size_t ci = 0; ci < row.cells.size() && logicalCol < numCols; ++ci) {
|
||||
auto& cell = row.cells[ci];
|
||||
const int cs = cell.colspan;
|
||||
|
||||
PageTableCellData cellData;
|
||||
cellData.xOffset = colXOffsets[logicalCol];
|
||||
cellData.columnWidth = spanFullCellWidth(logicalCol, cs);
|
||||
|
||||
if (cell.content && !cell.content->isEmpty()) {
|
||||
// Center-align cells that span the full table width (common for section headers/titles)
|
||||
if (cs >= static_cast<int>(numCols)) {
|
||||
BlockStyle centeredStyle = cell.content->getBlockStyle();
|
||||
centeredStyle.alignment = CssTextAlign::Center;
|
||||
centeredStyle.textAlignDefined = true;
|
||||
cell.content->setBlockStyle(centeredStyle);
|
||||
}
|
||||
|
||||
const uint16_t contentWidth = spanContentWidth(logicalCol, cs);
|
||||
std::vector<std::shared_ptr<TextBlock>> cellLines;
|
||||
|
||||
cell.content->layoutAndExtractLines(
|
||||
renderer, fontId, contentWidth,
|
||||
[&cellLines](const std::shared_ptr<TextBlock>& textBlock) { cellLines.push_back(textBlock); });
|
||||
|
||||
if (cellLines.size() > maxLinesInRow) {
|
||||
maxLinesInRow = cellLines.size();
|
||||
}
|
||||
cellData.lines = std::move(cellLines);
|
||||
}
|
||||
|
||||
cellDataVec.push_back(std::move(cellData));
|
||||
logicalCol += static_cast<size_t>(cs);
|
||||
}
|
||||
|
||||
// Fill remaining logical columns with empty cells (rows shorter than numCols)
|
||||
while (logicalCol < numCols) {
|
||||
PageTableCellData emptyCell;
|
||||
emptyCell.xOffset = colXOffsets[logicalCol];
|
||||
emptyCell.columnWidth = static_cast<uint16_t>(TABLE_CELL_PAD_X + colWidths[logicalCol] + TABLE_CELL_PAD_X);
|
||||
cellDataVec.push_back(std::move(emptyCell));
|
||||
logicalCol++;
|
||||
}
|
||||
|
||||
// Row height = max lines * lineHeight + top/bottom border + asymmetric vertical padding
|
||||
const int16_t rowHeight = static_cast<int16_t>(
|
||||
static_cast<int>(maxLinesInRow) * lh + 2 + TABLE_CELL_PAD_TOP + TABLE_CELL_PAD_BOTTOM);
|
||||
|
||||
auto pageTableRow = std::make_shared<PageTableRow>(
|
||||
std::move(cellDataVec), rowHeight, totalTableWidth, static_cast<int16_t>(lh), 0, 0);
|
||||
|
||||
addTableRowToPage(std::move(pageTableRow));
|
||||
}
|
||||
|
||||
// Add a small gap after the table
|
||||
if (extraParagraphSpacing) {
|
||||
currentPageNextY += lh / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
#include <memory>
|
||||
|
||||
#include "../ParsedText.h"
|
||||
#include "../TableData.h"
|
||||
#include "../blocks/ImageBlock.h"
|
||||
#include "../blocks/TextBlock.h"
|
||||
#include "../css/CssParser.h"
|
||||
#include "../css/CssStyle.h"
|
||||
|
||||
class Page;
|
||||
class PageTableRow;
|
||||
class GfxRenderer;
|
||||
class Epub;
|
||||
|
||||
@@ -65,16 +63,10 @@ class ChapterHtmlSlimParser {
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
|
||||
// Table buffering state
|
||||
bool inTable = false;
|
||||
std::unique_ptr<TableData> tableData;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
void processTable();
|
||||
void addTableRowToPage(std::shared_ptr<PageTableRow> row);
|
||||
// XML callbacks
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||
|
||||
@@ -296,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 {
|
||||
LOG_DBG("COF", "Skipping non-text reference in guide: %s", 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)) {
|
||||
LOG_DBG("COF", "Found %s reference in guide: %s.", 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;
|
||||
}
|
||||
@@ -327,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
|
||||
|
||||
|
||||
@@ -104,20 +104,3 @@ uint8_t quantize1bit(int gray, int x, int y) {
|
||||
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
|
||||
return (gray >= adjustedThreshold) ? 1 : 0;
|
||||
}
|
||||
|
||||
// Noise dithering for gradient fills - always uses hash-based noise regardless of global dithering config.
|
||||
// Produces smooth-looking gradients on the 4-level e-ink display.
|
||||
uint8_t quantizeNoiseDither(int gray, int x, int y) {
|
||||
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||
const int threshold = static_cast<int>(hash >> 24);
|
||||
|
||||
const int scaled = gray * 3;
|
||||
if (scaled < 255) {
|
||||
return (scaled + threshold >= 255) ? 1 : 0;
|
||||
} else if (scaled < 510) {
|
||||
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
||||
} else {
|
||||
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ uint8_t quantize(int gray, int x, int y);
|
||||
uint8_t quantizeSimple(int gray);
|
||||
uint8_t quantize1bit(int gray, int x, int y);
|
||||
int adjustPixel(int gray);
|
||||
uint8_t quantizeNoiseDither(int gray, int x, int y);
|
||||
|
||||
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
|
||||
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
|
||||
|
||||
@@ -73,16 +73,6 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawPixelGray(const int x, const int y, const uint8_t val2bit) const {
|
||||
if (renderMode == BW && val2bit < 3) {
|
||||
drawPixel(x, y);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val2bit == 1 || val2bit == 2)) {
|
||||
drawPixel(x, y, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val2bit == 1) {
|
||||
drawPixel(x, y, false);
|
||||
}
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
@@ -433,20 +423,12 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
|
||||
const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth();
|
||||
const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight();
|
||||
|
||||
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
||||
if (maxWidth > 0 && maxHeight > 0) {
|
||||
const float scaleX = static_cast<float>(maxWidth) / effectiveWidth;
|
||||
const float scaleY = static_cast<float>(maxHeight) / effectiveHeight;
|
||||
scale = std::min(scaleX, scaleY);
|
||||
isScaled = (scale < 0.999f || scale > 1.001f);
|
||||
} else if (maxWidth > 0 && effectiveWidth > static_cast<float>(maxWidth)) {
|
||||
scale = static_cast<float>(maxWidth) / effectiveWidth;
|
||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||
isScaled = true;
|
||||
} else if (maxHeight > 0 && effectiveHeight > static_cast<float>(maxHeight)) {
|
||||
scale = static_cast<float>(maxHeight) / effectiveHeight;
|
||||
}
|
||||
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||
isScaled = true;
|
||||
}
|
||||
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
||||
@@ -467,17 +449,12 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
||||
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
||||
// Screen's (0, 0) is the top-left corner.
|
||||
const int logicalY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||
int screenYStart, screenYEnd;
|
||||
int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||
if (isScaled) {
|
||||
screenYStart = static_cast<int>(std::floor(logicalY * scale)) + y;
|
||||
screenYEnd = static_cast<int>(std::floor((logicalY + 1) * scale)) + y;
|
||||
} else {
|
||||
screenYStart = logicalY + y;
|
||||
screenYEnd = screenYStart + 1;
|
||||
screenY = std::floor(screenY * scale);
|
||||
}
|
||||
|
||||
if (screenYStart >= getScreenHeight()) {
|
||||
screenY += y; // the offset should not be scaled
|
||||
if (screenY >= getScreenHeight()) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -488,7 +465,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenYEnd <= 0) {
|
||||
if (screenY < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -497,42 +474,27 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
continue;
|
||||
}
|
||||
|
||||
const int syStart = std::max(screenYStart, 0);
|
||||
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
||||
|
||||
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||
const int outX = bmpX - cropPixX;
|
||||
int screenXStart, screenXEnd;
|
||||
int screenX = bmpX - cropPixX;
|
||||
if (isScaled) {
|
||||
screenXStart = static_cast<int>(std::floor(outX * scale)) + x;
|
||||
screenXEnd = static_cast<int>(std::floor((outX + 1) * scale)) + x;
|
||||
} else {
|
||||
screenXStart = outX + x;
|
||||
screenXEnd = screenXStart + 1;
|
||||
screenX = std::floor(screenX * scale);
|
||||
}
|
||||
|
||||
if (screenXStart >= getScreenWidth()) {
|
||||
screenX += x; // the offset should not be scaled
|
||||
if (screenX >= getScreenWidth()) {
|
||||
break;
|
||||
}
|
||||
if (screenXEnd <= 0) {
|
||||
if (screenX < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
||||
|
||||
const int sxStart = std::max(screenXStart, 0);
|
||||
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
||||
|
||||
for (int sy = syStart; sy < syEnd; sy++) {
|
||||
for (int sx = sxStart; sx < sxEnd; sx++) {
|
||||
if (renderMode == BW && val < 3) {
|
||||
drawPixel(sx, sy);
|
||||
drawPixel(screenX, screenY);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
||||
drawPixel(sx, sy, false);
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||
drawPixel(sx, sy, false);
|
||||
}
|
||||
}
|
||||
drawPixel(screenX, screenY, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,16 +507,11 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
const int maxHeight) const {
|
||||
float scale = 1.0f;
|
||||
bool isScaled = false;
|
||||
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
||||
if (maxWidth > 0 && maxHeight > 0) {
|
||||
const float scaleX = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||
const float scaleY = static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight());
|
||||
scale = std::min(scaleX, scaleY);
|
||||
isScaled = (scale < 0.999f || scale > 1.001f);
|
||||
} else if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
||||
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||
isScaled = true;
|
||||
} else if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||
}
|
||||
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
||||
isScaled = true;
|
||||
}
|
||||
@@ -582,37 +539,20 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
|
||||
// Calculate screen Y based on whether BMP is top-down or bottom-up
|
||||
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||
int screenYStart, screenYEnd;
|
||||
if (isScaled) {
|
||||
screenYStart = static_cast<int>(std::floor(bmpYOffset * scale)) + y;
|
||||
screenYEnd = static_cast<int>(std::floor((bmpYOffset + 1) * scale)) + y;
|
||||
} else {
|
||||
screenYStart = bmpYOffset + y;
|
||||
screenYEnd = screenYStart + 1;
|
||||
}
|
||||
if (screenYStart >= getScreenHeight()) {
|
||||
int screenY = y + (isScaled ? static_cast<int>(std::floor(bmpYOffset * scale)) : bmpYOffset);
|
||||
if (screenY >= getScreenHeight()) {
|
||||
continue; // Continue reading to keep row counter in sync
|
||||
}
|
||||
if (screenYEnd <= 0) {
|
||||
if (screenY < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int syStart = std::max(screenYStart, 0);
|
||||
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
||||
|
||||
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
||||
int screenXStart, screenXEnd;
|
||||
if (isScaled) {
|
||||
screenXStart = static_cast<int>(std::floor(bmpX * scale)) + x;
|
||||
screenXEnd = static_cast<int>(std::floor((bmpX + 1) * scale)) + x;
|
||||
} else {
|
||||
screenXStart = bmpX + x;
|
||||
screenXEnd = screenXStart + 1;
|
||||
}
|
||||
if (screenXStart >= getScreenWidth()) {
|
||||
int screenX = x + (isScaled ? static_cast<int>(std::floor(bmpX * scale)) : bmpX);
|
||||
if (screenX >= getScreenWidth()) {
|
||||
break;
|
||||
}
|
||||
if (screenXEnd <= 0) {
|
||||
if (screenX < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -622,13 +562,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
|
||||
// val < 3 means black pixel (draw it)
|
||||
if (val < 3) {
|
||||
const int sxStart = std::max(screenXStart, 0);
|
||||
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
||||
for (int sy = syStart; sy < syEnd; sy++) {
|
||||
for (int sx = sxStart; sx < sxEnd; sx++) {
|
||||
drawPixel(sx, sy, true);
|
||||
}
|
||||
}
|
||||
drawPixel(screenX, screenY, true);
|
||||
}
|
||||
// White pixels (val == 3) are not drawn (leave background)
|
||||
}
|
||||
@@ -726,23 +660,6 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: Display only a rectangular region with specified refresh mode
|
||||
void GfxRenderer::displayWindow(int x, int y, int width, int height,
|
||||
HalDisplay::RefreshMode mode) const {
|
||||
LOG_DBG("GFX", "Displaying window at (%d,%d) size (%dx%d) with mode %d", x, y, width, height,
|
||||
static_cast<int>(mode));
|
||||
|
||||
// Validate bounds
|
||||
if (x < 0 || y < 0 || x + width > getScreenWidth() || y + height > getScreenHeight()) {
|
||||
LOG_ERR("GFX", "Window bounds exceed display dimensions!");
|
||||
return;
|
||||
}
|
||||
|
||||
display.displayWindow(static_cast<uint16_t>(x), static_cast<uint16_t>(y),
|
||||
static_cast<uint16_t>(width), static_cast<uint16_t>(height), mode,
|
||||
fadingFix);
|
||||
}
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
if (!text || maxWidth <= 0) return "";
|
||||
@@ -923,92 +840,6 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
const EpdFontFamily::Style style) const {
|
||||
// Cannot draw a NULL / empty string
|
||||
if (text == nullptr || *text == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
|
||||
// No printable characters
|
||||
if (!font.hasPrintableChars(text, style)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For 90° counter-clockwise rotation:
|
||||
// Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction
|
||||
// Text reads from top to bottom
|
||||
|
||||
const int advanceY = font.getData(style)->advanceY;
|
||||
const int ascender = font.getData(style)->ascender;
|
||||
|
||||
int yPos = y; // Current Y position (increases as we draw characters)
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
if (!glyph) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int is2Bit = font.getData(style)->is2Bit;
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
const int top = glyph->top;
|
||||
|
||||
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
for (int glyphX = 0; glyphX < width; glyphX++) {
|
||||
const int pixelPosition = glyphY * width + glyphX;
|
||||
|
||||
// 90° counter-clockwise rotation transformation:
|
||||
// screenX = mirrored CW X (right-to-left within advanceY span)
|
||||
// screenY = yPos + (left + glyphX) (downward)
|
||||
const int screenX = x + advanceY - 1 - (ascender - top + glyphY);
|
||||
const int screenY = yPos + left + glyphX;
|
||||
|
||||
if (is2Bit) {
|
||||
const uint8_t byte = bitmap[pixelPosition / 4];
|
||||
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
||||
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
||||
|
||||
if (renderMode == BW && bmpVal < 3) {
|
||||
drawPixel(screenX, screenY, black);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
}
|
||||
} else {
|
||||
const uint8_t byte = bitmap[pixelPosition / 8];
|
||||
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
||||
|
||||
if ((byte >> bit_index) & 1) {
|
||||
drawPixel(screenX, screenY, black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next character position (going down, so increase Y)
|
||||
yPos += glyph->advanceX;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||
|
||||
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||
|
||||
@@ -70,15 +70,13 @@ class GfxRenderer {
|
||||
int getScreenHeight() const;
|
||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||
void displayWindow(int x, int y, int width, int height,
|
||||
HalDisplay::RefreshMode mode = HalDisplay::FAST_REFRESH) const;
|
||||
// void displayWindow(int x, int y, int width, int height) const;
|
||||
void invertScreen() const;
|
||||
void clearScreen(uint8_t color = 0xFF) const;
|
||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||
|
||||
// Drawing
|
||||
void drawPixel(int x, int y, bool state = true) const;
|
||||
void drawPixelGray(int x, int y, uint8_t val2bit) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
|
||||
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
||||
@@ -112,11 +110,9 @@ class GfxRenderer {
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
|
||||
// Helpers for drawing rotated text (for side buttons)
|
||||
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
|
||||
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
void drawTextRotated90CCW(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getTextHeight(int fontId) const;
|
||||
|
||||
// Grayscale functions
|
||||
|
||||
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,27 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Book icon: 48x48, 1-bit packed (MSB first)
|
||||
// 0 = black, 1 = white (same format as Logo120.h)
|
||||
static constexpr int BOOK_ICON_WIDTH = 48;
|
||||
static constexpr int BOOK_ICON_HEIGHT = 48;
|
||||
static const uint8_t BookIcon[] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00,
|
||||
0x00, 0x1f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1c, 0x00, 0x00, 0x01, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1c, 0x00, 0x01,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1c, 0x00, 0x00, 0x1f, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff,
|
||||
0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f,
|
||||
0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f,
|
||||
0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00,
|
||||
0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
};
|
||||
@@ -1,480 +0,0 @@
|
||||
#include "PlaceholderCoverGenerator.h"
|
||||
|
||||
#include <EpdFont.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
// Include the UI fonts directly for self-contained placeholder rendering.
|
||||
// These are 1-bit bitmap fonts compiled from Ubuntu TTF.
|
||||
#include "builtinFonts/ubuntu_10_regular.h"
|
||||
#include "builtinFonts/ubuntu_12_bold.h"
|
||||
|
||||
// Book icon bitmap (48x48 1-bit, generated by scripts/generate_book_icon.py)
|
||||
#include "BookIcon.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// BMP writing helpers (same format as JpegToBmpConverter)
|
||||
inline void write16(Print& out, const uint16_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32(Print& out, const uint32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32Signed(Print& out, const int32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
|
||||
const int bytesPerRow = (width + 31) / 32 * 4;
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t fileSize = 62 + imageSize;
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize);
|
||||
write32(bmpOut, 0); // Reserved
|
||||
write32(bmpOut, 62); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height); // Negative = top-down
|
||||
write16(bmpOut, 1); // Color planes
|
||||
write16(bmpOut, 1); // Bits per pixel
|
||||
write32(bmpOut, 0); // BI_RGB
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835); // xPixelsPerMeter
|
||||
write32(bmpOut, 2835); // yPixelsPerMeter
|
||||
write32(bmpOut, 2); // colorsUsed
|
||||
write32(bmpOut, 2); // colorsImportant
|
||||
|
||||
// Palette: index 0 = black, index 1 = white
|
||||
const uint8_t palette[8] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Black
|
||||
0xFF, 0xFF, 0xFF, 0x00 // White
|
||||
};
|
||||
for (const uint8_t b : palette) {
|
||||
bmpOut.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// 1-bit pixel buffer that can render text, icons, and shapes, then write as BMP.
|
||||
class PixelBuffer {
|
||||
public:
|
||||
PixelBuffer(int width, int height) : width(width), height(height) {
|
||||
bytesPerRow = (width + 31) / 32 * 4;
|
||||
bufferSize = bytesPerRow * height;
|
||||
buffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
if (buffer) {
|
||||
memset(buffer, 0xFF, bufferSize); // White background
|
||||
}
|
||||
}
|
||||
|
||||
~PixelBuffer() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
bool isValid() const { return buffer != nullptr; }
|
||||
|
||||
/// Set a pixel to black.
|
||||
void setBlack(int x, int y) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
const int byteIndex = y * bytesPerRow + x / 8;
|
||||
const uint8_t bitMask = 0x80 >> (x % 8);
|
||||
buffer[byteIndex] &= ~bitMask;
|
||||
}
|
||||
|
||||
/// Set a scaled "pixel" (scale x scale block) to black.
|
||||
void setBlackScaled(int x, int y, int scale) {
|
||||
for (int dy = 0; dy < scale; dy++) {
|
||||
for (int dx = 0; dx < scale; dx++) {
|
||||
setBlack(x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a filled rectangle in black.
|
||||
void fillRect(int x, int y, int w, int h) {
|
||||
for (int row = y; row < y + h && row < height; row++) {
|
||||
for (int col = x; col < x + w && col < width; col++) {
|
||||
setBlack(col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a rectangular border in black.
|
||||
void drawBorder(int x, int y, int w, int h, int thickness) {
|
||||
fillRect(x, y, w, thickness); // Top
|
||||
fillRect(x, y + h - thickness, w, thickness); // Bottom
|
||||
fillRect(x, y, thickness, h); // Left
|
||||
fillRect(x + w - thickness, y, thickness, h); // Right
|
||||
}
|
||||
|
||||
/// Draw a horizontal line in black with configurable thickness.
|
||||
void drawHLine(int x, int y, int length, int thickness = 1) {
|
||||
fillRect(x, y, length, thickness);
|
||||
}
|
||||
|
||||
/// Render a single glyph at (cursorX, baselineY) with integer scaling. Returns advance in X (scaled).
|
||||
int renderGlyph(const EpdFontData* font, uint32_t codepoint, int cursorX, int baselineY, int scale = 1) {
|
||||
const EpdFont fontObj(font);
|
||||
const EpdGlyph* glyph = fontObj.getGlyph(codepoint);
|
||||
if (!glyph) {
|
||||
glyph = fontObj.getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
if (!glyph) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t* bitmap = &font->bitmap[glyph->dataOffset];
|
||||
const int glyphW = glyph->width;
|
||||
const int glyphH = glyph->height;
|
||||
|
||||
for (int gy = 0; gy < glyphH; gy++) {
|
||||
const int screenY = baselineY - glyph->top * scale + gy * scale;
|
||||
for (int gx = 0; gx < glyphW; gx++) {
|
||||
const int pixelPos = gy * glyphW + gx;
|
||||
const int screenX = cursorX + glyph->left * scale + gx * scale;
|
||||
|
||||
bool isSet = false;
|
||||
if (font->is2Bit) {
|
||||
const uint8_t byte = bitmap[pixelPos / 4];
|
||||
const uint8_t bitIndex = (3 - pixelPos % 4) * 2;
|
||||
const uint8_t val = 3 - ((byte >> bitIndex) & 0x3);
|
||||
isSet = (val < 3);
|
||||
} else {
|
||||
const uint8_t byte = bitmap[pixelPos / 8];
|
||||
const uint8_t bitIndex = 7 - (pixelPos % 8);
|
||||
isSet = ((byte >> bitIndex) & 1);
|
||||
}
|
||||
|
||||
if (isSet) {
|
||||
setBlackScaled(screenX, screenY, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return glyph->advanceX * scale;
|
||||
}
|
||||
|
||||
/// Render a UTF-8 string at (x, y) where y is the top of the text line, with integer scaling.
|
||||
void drawText(const EpdFontData* font, int x, int y, const char* text, int scale = 1) {
|
||||
const int baselineY = y + font->ascender * scale;
|
||||
int cursorX = x;
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
cursorX += renderGlyph(font, cp, cursorX, baselineY, scale);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a 1-bit icon bitmap (MSB first, 0=black, 1=white) with integer scaling.
|
||||
void drawIcon(const uint8_t* icon, int iconW, int iconH, int x, int y, int scale = 1) {
|
||||
const int bytesPerIconRow = iconW / 8;
|
||||
for (int iy = 0; iy < iconH; iy++) {
|
||||
for (int ix = 0; ix < iconW; ix++) {
|
||||
const int byteIdx = iy * bytesPerIconRow + ix / 8;
|
||||
const uint8_t bitMask = 0x80 >> (ix % 8);
|
||||
// In the icon data: 0 = black (drawn), 1 = white (skip)
|
||||
if (!(icon[byteIdx] & bitMask)) {
|
||||
const int sx = x + ix * scale;
|
||||
const int sy = y + iy * scale;
|
||||
setBlackScaled(sx, sy, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the pixel buffer to a file as a 1-bit BMP.
|
||||
bool writeBmp(Print& out) const {
|
||||
if (!buffer) return false;
|
||||
writeBmpHeader1bit(out, width, height);
|
||||
out.write(buffer, bufferSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
|
||||
private:
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
size_t bufferSize;
|
||||
uint8_t* buffer;
|
||||
};
|
||||
|
||||
/// Measure the width of a UTF-8 string in pixels (at 1x scale).
|
||||
int measureTextWidth(const EpdFontData* font, const char* text) {
|
||||
const EpdFont fontObj(font);
|
||||
int w = 0, h = 0;
|
||||
fontObj.getTextDimensions(text, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
/// Get the advance width of a single character.
|
||||
int getCharAdvance(const EpdFontData* font, uint32_t cp) {
|
||||
const EpdFont fontObj(font);
|
||||
const EpdGlyph* glyph = fontObj.getGlyph(cp);
|
||||
if (!glyph) return 0;
|
||||
return glyph->advanceX;
|
||||
}
|
||||
|
||||
/// Split a string into words (splitting on spaces).
|
||||
std::vector<std::string> splitWords(const std::string& text) {
|
||||
std::vector<std::string> words;
|
||||
std::string current;
|
||||
for (size_t i = 0; i < text.size(); i++) {
|
||||
if (text[i] == ' ') {
|
||||
if (!current.empty()) {
|
||||
words.push_back(current);
|
||||
current.clear();
|
||||
}
|
||||
} else {
|
||||
current += text[i];
|
||||
}
|
||||
}
|
||||
if (!current.empty()) {
|
||||
words.push_back(current);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
/// Word-wrap text into lines that fit within maxWidth pixels at the given scale.
|
||||
std::vector<std::string> wrapText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
|
||||
std::vector<std::string> lines;
|
||||
const auto words = splitWords(text);
|
||||
if (words.empty()) return lines;
|
||||
|
||||
const int spaceWidth = getCharAdvance(font, ' ') * scale;
|
||||
std::string currentLine;
|
||||
int currentWidth = 0;
|
||||
|
||||
for (const auto& word : words) {
|
||||
const int wordWidth = measureTextWidth(font, word.c_str()) * scale;
|
||||
|
||||
if (currentLine.empty()) {
|
||||
currentLine = word;
|
||||
currentWidth = wordWidth;
|
||||
} else if (currentWidth + spaceWidth + wordWidth <= maxWidth) {
|
||||
currentLine += " " + word;
|
||||
currentWidth += spaceWidth + wordWidth;
|
||||
} else {
|
||||
lines.push_back(currentLine);
|
||||
currentLine = word;
|
||||
currentWidth = wordWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentLine.empty()) {
|
||||
lines.push_back(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// Truncate a string with "..." if it exceeds maxWidth pixels at the given scale.
|
||||
std::string truncateText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
|
||||
if (measureTextWidth(font, text.c_str()) * scale <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string truncated = text;
|
||||
const char* ellipsis = "...";
|
||||
const int ellipsisWidth = measureTextWidth(font, ellipsis) * scale;
|
||||
|
||||
while (!truncated.empty()) {
|
||||
utf8RemoveLastChar(truncated);
|
||||
if (measureTextWidth(font, truncated.c_str()) * scale + ellipsisWidth <= maxWidth) {
|
||||
return truncated + ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
return ellipsis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PlaceholderCoverGenerator::generate(const std::string& outputPath, const std::string& title,
|
||||
const std::string& author, int width, int height) {
|
||||
LOG_DBG("PHC", "Generating placeholder cover %dx%d: \"%s\" by \"%s\"", width, height, title.c_str(), author.c_str());
|
||||
|
||||
const EpdFontData* titleFont = &ubuntu_12_bold;
|
||||
const EpdFontData* authorFont = &ubuntu_10_regular;
|
||||
|
||||
PixelBuffer buf(width, height);
|
||||
if (!buf.isValid()) {
|
||||
LOG_ERR("PHC", "Failed to allocate %dx%d pixel buffer (%d bytes)", width, height,
|
||||
(width + 31) / 32 * 4 * height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Proportional layout constants based on cover dimensions.
|
||||
// The device bezel covers ~2-3px on each edge, so we pad inward from the edge.
|
||||
const int edgePadding = std::max(3, width / 48); // ~10px at 480w, ~3px at 136w
|
||||
const int borderWidth = std::max(2, width / 96); // ~5px at 480w, ~2px at 136w
|
||||
const int innerPadding = std::max(4, width / 32); // ~15px at 480w, ~4px at 136w
|
||||
|
||||
// Text scaling: 2x for full-size covers, 1x for thumbnails
|
||||
const int titleScale = (height >= 600) ? 2 : 1;
|
||||
const int authorScale = (height >= 600) ? 2 : 1; // Author also larger on full covers
|
||||
// Icon: 2x for full cover, 1x for medium thumb, skip for small
|
||||
const int iconScale = (height >= 600) ? 2 : (height >= 350 ? 1 : 0);
|
||||
|
||||
// Draw border inset from edge
|
||||
buf.drawBorder(edgePadding, edgePadding, width - 2 * edgePadding, height - 2 * edgePadding, borderWidth);
|
||||
|
||||
// Content area (inside border + inner padding)
|
||||
const int contentX = edgePadding + borderWidth + innerPadding;
|
||||
const int contentY = edgePadding + borderWidth + innerPadding;
|
||||
const int contentW = width - 2 * contentX;
|
||||
const int contentH = height - 2 * contentY;
|
||||
|
||||
if (contentW <= 0 || contentH <= 0) {
|
||||
LOG_ERR("PHC", "Cover too small for content (%dx%d)", width, height);
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
|
||||
return false;
|
||||
}
|
||||
buf.writeBmp(file);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Layout zones ---
|
||||
// Title zone: top 2/3 of content area (icon + title)
|
||||
// Author zone: bottom 1/3 of content area
|
||||
const int titleZoneH = contentH * 2 / 3;
|
||||
const int authorZoneH = contentH - titleZoneH;
|
||||
const int authorZoneY = contentY + titleZoneH;
|
||||
|
||||
// --- Separator line at the zone boundary ---
|
||||
const int separatorWidth = contentW / 3;
|
||||
const int separatorX = contentX + (contentW - separatorWidth) / 2;
|
||||
buf.drawHLine(separatorX, authorZoneY, separatorWidth);
|
||||
|
||||
// --- Icon dimensions (needed for title text wrapping) ---
|
||||
const int iconW = (iconScale > 0) ? BOOK_ICON_WIDTH * iconScale : 0;
|
||||
const int iconGap = (iconScale > 0) ? std::max(8, width / 40) : 0; // Gap between icon and title text
|
||||
const int titleTextW = contentW - iconW - iconGap; // Title wraps in narrower area beside icon
|
||||
|
||||
// --- Prepare title text (wraps within the area to the right of the icon) ---
|
||||
const std::string displayTitle = title.empty() ? "Untitled" : title;
|
||||
auto titleLines = wrapText(titleFont, displayTitle, titleTextW, titleScale);
|
||||
|
||||
constexpr int MAX_TITLE_LINES = 5;
|
||||
if (static_cast<int>(titleLines.size()) > MAX_TITLE_LINES) {
|
||||
titleLines.resize(MAX_TITLE_LINES);
|
||||
titleLines.back() = truncateText(titleFont, titleLines.back(), titleTextW, titleScale);
|
||||
}
|
||||
|
||||
// --- Prepare author text (multi-line, max 3 lines) ---
|
||||
std::vector<std::string> authorLines;
|
||||
if (!author.empty()) {
|
||||
authorLines = wrapText(authorFont, author, contentW, authorScale);
|
||||
constexpr int MAX_AUTHOR_LINES = 3;
|
||||
if (static_cast<int>(authorLines.size()) > MAX_AUTHOR_LINES) {
|
||||
authorLines.resize(MAX_AUTHOR_LINES);
|
||||
authorLines.back() = truncateText(authorFont, authorLines.back(), contentW, authorScale);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Calculate title zone layout (icon LEFT of title) ---
|
||||
// Tighter line spacing so 2-3 title lines fit within the icon height
|
||||
const int titleLineH = titleFont->advanceY * titleScale * 3 / 4;
|
||||
const int iconH = (iconScale > 0) ? BOOK_ICON_HEIGHT * iconScale : 0;
|
||||
const int numTitleLines = static_cast<int>(titleLines.size());
|
||||
// Visual height: distance from top of first line to bottom of last line's glyphs.
|
||||
// Use ascender (not full advanceY) for the last line since trailing line-gap isn't visible.
|
||||
const int titleVisualH = (numTitleLines > 0)
|
||||
? (numTitleLines - 1) * titleLineH + titleFont->ascender * titleScale
|
||||
: 0;
|
||||
const int titleBlockH = std::max(iconH, titleVisualH); // Taller of icon or text
|
||||
|
||||
int titleStartY = contentY + (titleZoneH - titleBlockH) / 2;
|
||||
if (titleStartY < contentY) {
|
||||
titleStartY = contentY;
|
||||
}
|
||||
|
||||
// If title fits within icon height, center it vertically against the icon.
|
||||
// Otherwise top-align so extra lines overflow below.
|
||||
const int iconY = titleStartY;
|
||||
const int titleTextY = (iconH > 0 && titleVisualH <= iconH)
|
||||
? titleStartY + (iconH - titleVisualH) / 2
|
||||
: titleStartY;
|
||||
|
||||
// --- Horizontal centering: measure the widest title line, then center icon+gap+text block ---
|
||||
int maxTitleLineW = 0;
|
||||
for (const auto& line : titleLines) {
|
||||
const int w = measureTextWidth(titleFont, line.c_str()) * titleScale;
|
||||
if (w > maxTitleLineW) maxTitleLineW = w;
|
||||
}
|
||||
const int titleBlockW = iconW + iconGap + maxTitleLineW;
|
||||
const int titleBlockX = contentX + (contentW - titleBlockW) / 2;
|
||||
|
||||
// --- Draw icon ---
|
||||
if (iconScale > 0) {
|
||||
buf.drawIcon(BookIcon, BOOK_ICON_WIDTH, BOOK_ICON_HEIGHT, titleBlockX, iconY, iconScale);
|
||||
}
|
||||
|
||||
// --- Draw title lines (to the right of the icon) ---
|
||||
const int titleTextX = titleBlockX + iconW + iconGap;
|
||||
int currentY = titleTextY;
|
||||
for (const auto& line : titleLines) {
|
||||
buf.drawText(titleFont, titleTextX, currentY, line.c_str(), titleScale);
|
||||
currentY += titleLineH;
|
||||
}
|
||||
|
||||
// --- Draw author lines (centered vertically in bottom 1/3, centered horizontally) ---
|
||||
if (!authorLines.empty()) {
|
||||
const int authorLineH = authorFont->advanceY * authorScale;
|
||||
const int authorBlockH = static_cast<int>(authorLines.size()) * authorLineH;
|
||||
int authorStartY = authorZoneY + (authorZoneH - authorBlockH) / 2;
|
||||
if (authorStartY < authorZoneY + 4) {
|
||||
authorStartY = authorZoneY + 4; // Small gap below separator
|
||||
}
|
||||
|
||||
for (const auto& line : authorLines) {
|
||||
const int lineWidth = measureTextWidth(authorFont, line.c_str()) * authorScale;
|
||||
const int lineX = contentX + (contentW - lineWidth) / 2;
|
||||
buf.drawText(authorFont, lineX, authorStartY, line.c_str(), authorScale);
|
||||
authorStartY += authorLineH;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Write to file ---
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
|
||||
LOG_ERR("PHC", "Failed to open output file: %s", outputPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool success = buf.writeBmp(file);
|
||||
file.close();
|
||||
|
||||
if (success) {
|
||||
LOG_DBG("PHC", "Placeholder cover written to %s", outputPath.c_str());
|
||||
} else {
|
||||
LOG_ERR("PHC", "Failed to write placeholder BMP");
|
||||
Storage.remove(outputPath.c_str());
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/// Generates simple 1-bit BMP placeholder covers with title/author text
|
||||
/// for books that have no embedded cover image.
|
||||
class PlaceholderCoverGenerator {
|
||||
public:
|
||||
/// Generate a placeholder cover BMP with title and author text.
|
||||
/// The BMP is written to outputPath as a 1-bit black-and-white image.
|
||||
/// Returns true if the file was written successfully.
|
||||
static bool generate(const std::string& outputPath, const std::string& title, const std::string& author, int width,
|
||||
int height);
|
||||
};
|
||||
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);
|
||||
};
|
||||
@@ -97,9 +97,6 @@ std::string Txt::findCoverImage() const {
|
||||
|
||||
std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||
|
||||
std::string Txt::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||
std::string Txt::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||
|
||||
bool Txt::generateCoverBmp() const {
|
||||
// Already generated, return true
|
||||
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||
|
||||
@@ -28,10 +28,6 @@ class Txt {
|
||||
[[nodiscard]] bool generateCoverBmp() const;
|
||||
[[nodiscard]] std::string findCoverImage() const;
|
||||
|
||||
// Thumbnail paths (matching Epub/Xtc pattern for home screen covers)
|
||||
[[nodiscard]] std::string getThumbBmpPath() const;
|
||||
[[nodiscard]] std::string getThumbBmpPath(int height) const;
|
||||
|
||||
// Read content from file
|
||||
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
|
||||
};
|
||||
|
||||
@@ -32,13 +32,6 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
|
||||
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: Display only a rectangular region
|
||||
void HalDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
(void)mode; // EInkDisplay::displayWindow does not take mode yet
|
||||
einkDisplay.displayWindow(x, y, w, h, turnOffScreen);
|
||||
}
|
||||
|
||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@ class HalDisplay {
|
||||
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// EXPERIMENTAL: Display only a rectangular region
|
||||
void displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// Power management
|
||||
void deepSleep();
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include <HalGPIO.h>
|
||||
#include <SPI.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
void HalGPIO::begin() {
|
||||
inputMgr.begin();
|
||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
}
|
||||
|
||||
@@ -21,6 +23,23 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
||||
|
||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||
|
||||
void HalGPIO::startDeepSleep() {
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (inputMgr.isPressed(BTN_POWER)) {
|
||||
delay(50);
|
||||
inputMgr.update();
|
||||
}
|
||||
// Arm the wakeup trigger *after* the button is released
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
int HalGPIO::getBatteryPercentage() const {
|
||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||
return battery.readPercentage();
|
||||
}
|
||||
|
||||
bool HalGPIO::isUsbConnected() const {
|
||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||
return digitalRead(UART0_RXD) == HIGH;
|
||||
|
||||
@@ -38,6 +38,12 @@ class HalGPIO {
|
||||
bool wasAnyReleased() const;
|
||||
unsigned long getHeldTime() const;
|
||||
|
||||
// Setup wake up GPIO and enter deep sleep
|
||||
void startDeepSleep();
|
||||
|
||||
// Get battery percentage (range 0-100)
|
||||
int getBatteryPercentage() const;
|
||||
|
||||
// Check if USB is connected
|
||||
bool isUsbConnected() const;
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#include "HalPowerManager.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
|
||||
void HalPowerManager::begin() {
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
normalFreq = getCpuFrequencyMhz();
|
||||
}
|
||||
|
||||
void HalPowerManager::setPowerSaving(bool enabled) {
|
||||
if (normalFreq <= 0) {
|
||||
return; // invalid state
|
||||
}
|
||||
if (enabled && !isLowPower) {
|
||||
LOG_DBG("PWR", "Going to low-power mode");
|
||||
if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) {
|
||||
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!enabled && isLowPower) {
|
||||
LOG_DBG("PWR", "Restoring normal CPU frequency");
|
||||
if (!setCpuFrequencyMhz(normalFreq)) {
|
||||
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq);
|
||||
return;
|
||||
}
|
||||
}
|
||||
isLowPower = enabled;
|
||||
}
|
||||
|
||||
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||
delay(50);
|
||||
gpio.update();
|
||||
}
|
||||
// Arm the wakeup trigger *after* the button is released
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
int HalPowerManager::getBatteryPercentage() const {
|
||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||
return battery.readPercentage();
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BatteryMonitor.h>
|
||||
#include <InputManager.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
|
||||
class HalPowerManager {
|
||||
int normalFreq = 0; // MHz
|
||||
bool isLowPower = false;
|
||||
|
||||
public:
|
||||
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||
|
||||
void begin();
|
||||
|
||||
// Control CPU frequency for power saving
|
||||
void setPowerSaving(bool enabled);
|
||||
|
||||
// Setup wake up GPIO and enter deep sleep
|
||||
void startDeepSleep(HalGPIO& gpio) const;
|
||||
|
||||
// Get battery percentage (range 0-100)
|
||||
int getBatteryPercentage() const;
|
||||
};
|
||||
@@ -22,6 +22,7 @@ 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
|
||||
@@ -44,6 +45,7 @@ board_build.partitions = partitions.csv
|
||||
|
||||
extra_scripts =
|
||||
pre:scripts/build_html.py
|
||||
pre:scripts/gen_i18n.py
|
||||
|
||||
; Libraries
|
||||
lib_deps =
|
||||
@@ -65,22 +67,6 @@ build_flags =
|
||||
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
||||
|
||||
|
||||
[env:mod]
|
||||
extends = base
|
||||
extra_scripts =
|
||||
${base.extra_scripts}
|
||||
pre:scripts/inject_mod_version.py
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DOMIT_OPENDYSLEXIC
|
||||
-DOMIT_HYPH_DE
|
||||
-DOMIT_HYPH_ES
|
||||
-DOMIT_HYPH_FR
|
||||
-DOMIT_HYPH_IT
|
||||
-DOMIT_HYPH_RU
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2 ; Set log level to debug for mod builds
|
||||
|
||||
[env:gh_release]
|
||||
extends = base
|
||||
build_flags =
|
||||
|
||||
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
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a 1-bit book icon bitmap as a C header for PlaceholderCoverGenerator.
|
||||
|
||||
The icon is a simplified closed book with a spine on the left and 3 text lines.
|
||||
Output format matches Logo120.h: MSB-first packed 1-bit, 0=black, 1=white.
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import sys
|
||||
|
||||
|
||||
def generate_book_icon(size=48):
|
||||
"""Create a book icon at the given size."""
|
||||
img = Image.new("1", (size, size), 1) # White background
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Scale helper
|
||||
s = size / 48.0
|
||||
|
||||
# Book body (main rectangle, leaving room for spine and pages)
|
||||
body_left = int(6 * s)
|
||||
body_top = int(2 * s)
|
||||
body_right = int(42 * s)
|
||||
body_bottom = int(40 * s)
|
||||
|
||||
# Draw book body outline (2px thick)
|
||||
for i in range(int(2 * s)):
|
||||
draw.rectangle(
|
||||
[body_left + i, body_top + i, body_right - i, body_bottom - i], outline=0
|
||||
)
|
||||
|
||||
# Spine (thicker left edge)
|
||||
spine_width = int(4 * s)
|
||||
draw.rectangle([body_left, body_top, body_left + spine_width, body_bottom], fill=0)
|
||||
|
||||
# Pages at the bottom (slight offset from body)
|
||||
pages_top = body_bottom
|
||||
pages_bottom = int(44 * s)
|
||||
draw.rectangle(
|
||||
[body_left + int(2 * s), pages_top, body_right - int(1 * s), pages_bottom],
|
||||
outline=0,
|
||||
)
|
||||
# Page edges (a few lines)
|
||||
for i in range(3):
|
||||
y = pages_top + int((i + 1) * 1 * s)
|
||||
if y < pages_bottom:
|
||||
draw.line(
|
||||
[body_left + int(3 * s), y, body_right - int(2 * s), y], fill=0
|
||||
)
|
||||
|
||||
# Text lines on the book cover
|
||||
text_left = body_left + spine_width + int(4 * s)
|
||||
text_right = body_right - int(4 * s)
|
||||
line_thickness = max(1, int(1.5 * s))
|
||||
|
||||
text_lines_y = [int(12 * s), int(18 * s), int(24 * s)]
|
||||
text_widths = [1.0, 0.7, 0.85] # Relative widths for visual interest
|
||||
|
||||
for y, w_ratio in zip(text_lines_y, text_widths):
|
||||
line_right = text_left + int((text_right - text_left) * w_ratio)
|
||||
for t in range(line_thickness):
|
||||
draw.line([text_left, y + t, line_right, y + t], fill=0)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def image_to_c_array(img, name="BookIcon"):
|
||||
"""Convert a 1-bit PIL image to a C header array."""
|
||||
width, height = img.size
|
||||
pixels = img.load()
|
||||
|
||||
bytes_per_row = width // 8
|
||||
data = []
|
||||
|
||||
for y in range(height):
|
||||
for bx in range(bytes_per_row):
|
||||
byte = 0
|
||||
for bit in range(8):
|
||||
x = bx * 8 + bit
|
||||
if x < width:
|
||||
# 1 = white, 0 = black (matching Logo120.h convention)
|
||||
if pixels[x, y]:
|
||||
byte |= 1 << (7 - bit)
|
||||
data.append(byte)
|
||||
|
||||
# Format as C header
|
||||
lines = []
|
||||
lines.append("#pragma once")
|
||||
lines.append("#include <cstdint>")
|
||||
lines.append("")
|
||||
lines.append(f"// Book icon: {width}x{height}, 1-bit packed (MSB first)")
|
||||
lines.append(f"// 0 = black, 1 = white (same format as Logo120.h)")
|
||||
lines.append(f"static constexpr int BOOK_ICON_WIDTH = {width};")
|
||||
lines.append(f"static constexpr int BOOK_ICON_HEIGHT = {height};")
|
||||
lines.append(f"static const uint8_t {name}[] = {{")
|
||||
|
||||
# Format data in rows of 16 bytes
|
||||
for i in range(0, len(data), 16):
|
||||
chunk = data[i : i + 16]
|
||||
hex_str = ", ".join(f"0x{b:02x}" for b in chunk)
|
||||
lines.append(f" {hex_str},")
|
||||
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
size = int(sys.argv[1]) if len(sys.argv) > 1 else 48
|
||||
img = generate_book_icon(size)
|
||||
|
||||
# Save preview PNG
|
||||
preview_path = f"mod/book_icon_{size}x{size}.png"
|
||||
img.resize((size * 4, size * 4), Image.NEAREST).save(preview_path)
|
||||
print(f"Preview saved to {preview_path}", file=sys.stderr)
|
||||
|
||||
# Generate C header
|
||||
header = image_to_c_array(img, "BookIcon")
|
||||
output_path = "lib/PlaceholderCover/BookIcon.h"
|
||||
with open(output_path, "w") as f:
|
||||
f.write(header)
|
||||
print(f"C header saved to {output_path}", file=sys.stderr)
|
||||
@@ -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}),
|
||||
}};
|
||||
|
||||
@@ -84,6 +84,9 @@ def create_grayscale_test_image(filename, is_png=True):
|
||||
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"),
|
||||
@@ -104,12 +107,13 @@ def create_grayscale_test_image(filename, is_png=True):
|
||||
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
|
||||
# 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:
|
||||
@@ -166,6 +170,7 @@ 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)
|
||||
@@ -181,7 +186,7 @@ def create_scaling_test_image(filename, is_png=True):
|
||||
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
|
||||
# Grid pattern to verify scaling quality
|
||||
grid_start_y = 220
|
||||
grid_size = 400
|
||||
cell_size = 50
|
||||
@@ -198,7 +203,35 @@ def create_scaling_test_image(filename, is_png=True):
|
||||
else:
|
||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200)
|
||||
|
||||
# Pass/fail
|
||||
# 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)
|
||||
@@ -208,6 +241,73 @@ def create_scaling_test_image(filename, is_png=True):
|
||||
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.
|
||||
@@ -240,6 +340,67 @@ def create_cache_test_image(filename, page_num, is_png=True):
|
||||
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.
|
||||
@@ -362,18 +523,22 @@ def make_chapter(title, body_content):
|
||||
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)
|
||||
@@ -382,6 +547,8 @@ def main():
|
||||
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)
|
||||
@@ -395,6 +562,13 @@ def main():
|
||||
("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>
|
||||
@@ -405,21 +579,30 @@ def main():
|
||||
<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. Centering", make_chapter("Centering Test", """
|
||||
("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'])]),
|
||||
("4. Scaling", make_chapter("Scaling Test", """
|
||||
("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'])]),
|
||||
("5. Cache Test A", make_chapter("Cache Test - Page A", """
|
||||
("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'])]),
|
||||
("6. Cache Test B", make_chapter("Cache Test - Page B", """
|
||||
("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>
|
||||
@@ -433,6 +616,13 @@ def main():
|
||||
("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>
|
||||
@@ -443,21 +633,30 @@ def main():
|
||||
<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. Centering", make_chapter("Centering Test", """
|
||||
("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'])]),
|
||||
("4. Scaling", make_chapter("Scaling Test", """
|
||||
("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'])]),
|
||||
("5. Cache Test A", make_chapter("Cache Test - Page A", """
|
||||
("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'])]),
|
||||
("6. Cache Test B", make_chapter("Cache Test - Page B", """
|
||||
("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>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Import("env")
|
||||
import subprocess
|
||||
|
||||
config = env.GetProjectConfig()
|
||||
version = config.get("crosspoint", "version")
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
git_hash = result.stdout.strip()
|
||||
|
||||
env.Append(
|
||||
BUILD_FLAGS=[f'-DCROSSPOINT_VERSION=\\"{version}-mod+{git_hash}\\"']
|
||||
)
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a preview of the placeholder cover layout at full cover size (480x800).
|
||||
This mirrors the C++ PlaceholderCoverGenerator layout logic for visual verification.
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Reuse the book icon generator
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from generate_book_icon import generate_book_icon
|
||||
|
||||
|
||||
def create_preview(width=480, height=800, title="The Great Gatsby", author="F. Scott Fitzgerald"):
|
||||
img = Image.new("1", (width, height), 1) # White
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Proportional layout constants
|
||||
edge_padding = max(3, width // 48) # ~10px at 480w
|
||||
border_width = max(2, width // 96) # ~5px at 480w
|
||||
inner_padding = max(4, width // 32) # ~15px at 480w
|
||||
|
||||
title_scale = 2 if height >= 600 else 1
|
||||
author_scale = 2 if height >= 600 else 1 # Author also larger on full covers
|
||||
icon_scale = 2 if height >= 600 else (1 if height >= 350 else 0)
|
||||
|
||||
# Draw border inset from edge
|
||||
bx = edge_padding
|
||||
by = edge_padding
|
||||
bw = width - 2 * edge_padding
|
||||
bh = height - 2 * edge_padding
|
||||
for i in range(border_width):
|
||||
draw.rectangle([bx + i, by + i, bx + bw - 1 - i, by + bh - 1 - i], outline=0)
|
||||
|
||||
# Content area
|
||||
content_x = edge_padding + border_width + inner_padding
|
||||
content_y = edge_padding + border_width + inner_padding
|
||||
content_w = width - 2 * content_x
|
||||
content_h = height - 2 * content_y
|
||||
|
||||
# Zones
|
||||
title_zone_h = content_h * 2 // 3
|
||||
author_zone_h = content_h - title_zone_h
|
||||
author_zone_y = content_y + title_zone_h
|
||||
|
||||
# Separator
|
||||
sep_w = content_w // 3
|
||||
sep_x = content_x + (content_w - sep_w) // 2
|
||||
draw.line([sep_x, author_zone_y, sep_x + sep_w, author_zone_y], fill=0)
|
||||
|
||||
# Use a basic font for the preview (won't match exact Ubuntu metrics, but shows layout)
|
||||
try:
|
||||
title_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 12 * title_scale)
|
||||
author_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 10 * author_scale)
|
||||
except (OSError, IOError):
|
||||
title_font = ImageFont.load_default()
|
||||
author_font = ImageFont.load_default()
|
||||
|
||||
# Icon dimensions (needed for title text wrapping)
|
||||
icon_w_px = 48 * icon_scale if icon_scale > 0 else 0
|
||||
icon_h_px = 48 * icon_scale if icon_scale > 0 else 0
|
||||
icon_gap = max(8, width // 40) if icon_scale > 0 else 0
|
||||
title_text_w = content_w - icon_w_px - icon_gap # Title wraps in narrower area beside icon
|
||||
|
||||
# Wrap title (within the narrower area to the right of the icon)
|
||||
title_lines = []
|
||||
words = title.split()
|
||||
current_line = ""
|
||||
for word in words:
|
||||
test = f"{current_line} {word}".strip()
|
||||
bbox = draw.textbbox((0, 0), test, font=title_font)
|
||||
if bbox[2] - bbox[0] <= title_text_w:
|
||||
current_line = test
|
||||
else:
|
||||
if current_line:
|
||||
title_lines.append(current_line)
|
||||
current_line = word
|
||||
if current_line:
|
||||
title_lines.append(current_line)
|
||||
title_lines = title_lines[:5]
|
||||
|
||||
# Line spacing: 75% of advanceY (tighter so 2-3 lines fit within icon height)
|
||||
title_line_h = 29 * title_scale * 3 // 4 # Based on C++ ubuntu_12_bold advanceY
|
||||
|
||||
# Measure actual single-line height from the PIL font for accurate centering
|
||||
sample_bbox = draw.textbbox((0, 0), "Ag", font=title_font) # Tall + descender chars
|
||||
single_line_visual_h = sample_bbox[3] - sample_bbox[1]
|
||||
|
||||
# Visual height: line spacing between lines + actual height of last line's glyphs
|
||||
num_title_lines = len(title_lines)
|
||||
title_visual_h = (num_title_lines - 1) * title_line_h + single_line_visual_h if num_title_lines > 0 else 0
|
||||
title_block_h = max(icon_h_px, title_visual_h)
|
||||
|
||||
title_start_y = content_y + (title_zone_h - title_block_h) // 2
|
||||
if title_start_y < content_y:
|
||||
title_start_y = content_y
|
||||
|
||||
# If title fits within icon height, center it vertically against the icon.
|
||||
# Otherwise top-align so extra lines overflow below.
|
||||
icon_y = title_start_y
|
||||
if icon_h_px > 0 and title_visual_h <= icon_h_px:
|
||||
title_text_y = title_start_y + (icon_h_px - title_visual_h) // 2
|
||||
else:
|
||||
title_text_y = title_start_y
|
||||
|
||||
# Horizontal centering: measure widest title line, center icon+gap+text block
|
||||
max_title_line_w = 0
|
||||
for line in title_lines:
|
||||
bbox = draw.textbbox((0, 0), line, font=title_font)
|
||||
w = bbox[2] - bbox[0]
|
||||
if w > max_title_line_w:
|
||||
max_title_line_w = w
|
||||
title_block_w = icon_w_px + icon_gap + max_title_line_w
|
||||
title_block_x = content_x + (content_w - title_block_w) // 2
|
||||
|
||||
# Draw icon
|
||||
if icon_scale > 0:
|
||||
icon_img = generate_book_icon(48)
|
||||
scaled_icon = icon_img.resize((icon_w_px, icon_h_px), Image.NEAREST)
|
||||
for iy in range(scaled_icon.height):
|
||||
for ix in range(scaled_icon.width):
|
||||
if not scaled_icon.getpixel((ix, iy)):
|
||||
img.putpixel((title_block_x + ix, icon_y + iy), 0)
|
||||
|
||||
# Draw title (to the right of the icon)
|
||||
title_text_x = title_block_x + icon_w_px + icon_gap
|
||||
current_y = title_text_y
|
||||
for line in title_lines:
|
||||
draw.text((title_text_x, current_y), line, fill=0, font=title_font)
|
||||
current_y += title_line_h
|
||||
|
||||
# Wrap author
|
||||
author_lines = []
|
||||
words = author.split()
|
||||
current_line = ""
|
||||
for word in words:
|
||||
test = f"{current_line} {word}".strip()
|
||||
bbox = draw.textbbox((0, 0), test, font=author_font)
|
||||
if bbox[2] - bbox[0] <= content_w:
|
||||
current_line = test
|
||||
else:
|
||||
if current_line:
|
||||
author_lines.append(current_line)
|
||||
current_line = word
|
||||
if current_line:
|
||||
author_lines.append(current_line)
|
||||
author_lines = author_lines[:3]
|
||||
|
||||
# Draw author centered in bottom 1/3
|
||||
author_line_h = 24 * author_scale # Ubuntu 10 regular advanceY ~24
|
||||
author_block_h = len(author_lines) * author_line_h
|
||||
author_start_y = author_zone_y + (author_zone_h - author_block_h) // 2
|
||||
|
||||
for line in author_lines:
|
||||
bbox = draw.textbbox((0, 0), line, font=author_font)
|
||||
line_w = bbox[2] - bbox[0]
|
||||
line_x = content_x + (content_w - line_w) // 2
|
||||
draw.text((line_x, author_start_y), line, fill=0, font=author_font)
|
||||
author_start_y += author_line_h
|
||||
|
||||
return img
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Full cover
|
||||
img = create_preview(480, 800, "A Really Long Book Title That Should Wrap", "Jane Doe")
|
||||
img.save("mod/preview_cover_480x800.png")
|
||||
print("Saved mod/preview_cover_480x800.png", file=sys.stderr)
|
||||
|
||||
# Medium thumbnail
|
||||
img2 = create_preview(240, 400, "A Really Long Book Title That Should Wrap", "Jane Doe")
|
||||
img2.save("mod/preview_thumb_240x400.png")
|
||||
print("Saved mod/preview_thumb_240x400.png", file=sys.stderr)
|
||||
|
||||
# Small thumbnail
|
||||
img3 = create_preview(136, 226, "A Really Long Book Title", "Jane Doe")
|
||||
img3.save("mod/preview_thumb_136x226.png")
|
||||
print("Saved mod/preview_thumb_136x226.png", file=sys.stderr)
|
||||
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
|
||||
@@ -5,6 +5,7 @@
|
||||
#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 = 31;
|
||||
// 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,6 +77,68 @@ 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
|
||||
Storage.mkdir("/.crosspoint");
|
||||
@@ -86,40 +148,15 @@ bool CrossPointSettings::saveToFile() const {
|
||||
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);
|
||||
serialization::writePod(outputFile, sleepScreenLetterboxFill);
|
||||
// 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();
|
||||
|
||||
LOG_DBG("CPS", "Settings saved to file");
|
||||
@@ -224,10 +261,6 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, embeddedStyle);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sleepScreenLetterboxFill, SLEEP_SCREEN_LETTERBOX_FILL_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{ uint8_t _ignore; serialization::readPod(inputFile, _ignore); } // legacy: sleepScreenGradientDir
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
// New fields added at end for backward compatibility
|
||||
} while (false);
|
||||
|
||||
@@ -244,8 +277,8 @@ bool CrossPointSettings::loadFromFile() {
|
||||
|
||||
float CrossPointSettings::getReaderLineCompression() const {
|
||||
switch (fontFamily) {
|
||||
#ifndef OMIT_BOOKERLY
|
||||
case BOOKERLY:
|
||||
default:
|
||||
switch (lineSpacing) {
|
||||
case TIGHT:
|
||||
return 0.95f;
|
||||
@@ -255,8 +288,6 @@ float CrossPointSettings::getReaderLineCompression() const {
|
||||
case WIDE:
|
||||
return 1.1f;
|
||||
}
|
||||
#endif // OMIT_BOOKERLY
|
||||
#ifndef OMIT_NOTOSANS
|
||||
case NOTOSANS:
|
||||
switch (lineSpacing) {
|
||||
case TIGHT:
|
||||
@@ -267,8 +298,6 @@ float CrossPointSettings::getReaderLineCompression() const {
|
||||
case WIDE:
|
||||
return 1.0f;
|
||||
}
|
||||
#endif // OMIT_NOTOSANS
|
||||
#ifndef OMIT_OPENDYSLEXIC
|
||||
case OPENDYSLEXIC:
|
||||
switch (lineSpacing) {
|
||||
case TIGHT:
|
||||
@@ -279,30 +308,6 @@ float CrossPointSettings::getReaderLineCompression() const {
|
||||
case WIDE:
|
||||
return 1.0f;
|
||||
}
|
||||
#endif // OMIT_OPENDYSLEXIC
|
||||
default:
|
||||
// Fallback: use Bookerly-style compression, or Noto Sans if Bookerly is omitted
|
||||
#if !defined(OMIT_BOOKERLY)
|
||||
switch (lineSpacing) {
|
||||
case TIGHT:
|
||||
return 0.95f;
|
||||
case NORMAL:
|
||||
default:
|
||||
return 1.0f;
|
||||
case WIDE:
|
||||
return 1.1f;
|
||||
}
|
||||
#else
|
||||
switch (lineSpacing) {
|
||||
case TIGHT:
|
||||
return 0.90f;
|
||||
case NORMAL:
|
||||
default:
|
||||
return 0.95f;
|
||||
case WIDE:
|
||||
return 1.0f;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,8 +345,8 @@ int CrossPointSettings::getRefreshFrequency() const {
|
||||
|
||||
int CrossPointSettings::getReaderFontId() const {
|
||||
switch (fontFamily) {
|
||||
#ifndef OMIT_BOOKERLY
|
||||
case BOOKERLY:
|
||||
default:
|
||||
switch (fontSize) {
|
||||
case SMALL:
|
||||
return BOOKERLY_12_FONT_ID;
|
||||
@@ -353,8 +358,6 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
case EXTRA_LARGE:
|
||||
return BOOKERLY_18_FONT_ID;
|
||||
}
|
||||
#endif // OMIT_BOOKERLY
|
||||
#ifndef OMIT_NOTOSANS
|
||||
case NOTOSANS:
|
||||
switch (fontSize) {
|
||||
case SMALL:
|
||||
@@ -367,8 +370,6 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
case EXTRA_LARGE:
|
||||
return NOTOSANS_18_FONT_ID;
|
||||
}
|
||||
#endif // OMIT_NOTOSANS
|
||||
#ifndef OMIT_OPENDYSLEXIC
|
||||
case OPENDYSLEXIC:
|
||||
switch (fontSize) {
|
||||
case SMALL:
|
||||
@@ -381,17 +382,5 @@ int CrossPointSettings::getReaderFontId() const {
|
||||
case EXTRA_LARGE:
|
||||
return OPENDYSLEXIC_14_FONT_ID;
|
||||
}
|
||||
#endif // OMIT_OPENDYSLEXIC
|
||||
default:
|
||||
// Fallback to first available font family at medium size
|
||||
#if !defined(OMIT_BOOKERLY)
|
||||
return BOOKERLY_14_FONT_ID;
|
||||
#elif !defined(OMIT_NOTOSANS)
|
||||
return NOTOSANS_14_FONT_ID;
|
||||
#elif !defined(OMIT_OPENDYSLEXIC)
|
||||
return OPENDYSLEXIC_10_FONT_ID;
|
||||
#else
|
||||
#error "At least one font family must be available"
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
|
||||
// Forward declarations
|
||||
class FsFile;
|
||||
|
||||
class CrossPointSettings {
|
||||
private:
|
||||
// Private constructor for singleton
|
||||
@@ -31,12 +34,6 @@ class CrossPointSettings {
|
||||
INVERTED_BLACK_AND_WHITE = 2,
|
||||
SLEEP_SCREEN_COVER_FILTER_COUNT
|
||||
};
|
||||
enum SLEEP_SCREEN_LETTERBOX_FILL {
|
||||
LETTERBOX_DITHERED = 0,
|
||||
LETTERBOX_SOLID = 1,
|
||||
LETTERBOX_NONE = 2,
|
||||
SLEEP_SCREEN_LETTERBOX_FILL_COUNT
|
||||
};
|
||||
|
||||
// Status bar display type enum
|
||||
enum STATUS_BAR_MODE {
|
||||
@@ -131,8 +128,6 @@ class CrossPointSettings {
|
||||
uint8_t sleepScreenCoverMode = FIT;
|
||||
// Sleep screen cover filter
|
||||
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
||||
// Sleep screen letterbox fill mode (Dithered / Solid / None)
|
||||
uint8_t sleepScreenLetterboxFill = LETTERBOX_DITHERED;
|
||||
// Status bar settings
|
||||
uint8_t statusBar = FULL;
|
||||
// Text rendering settings
|
||||
@@ -190,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();
|
||||
|
||||
|
||||
@@ -38,15 +38,6 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
void RecentBooksStore::removeBook(const std::string& path) {
|
||||
auto it =
|
||||
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
||||
if (it != recentBooks.end()) {
|
||||
recentBooks.erase(it);
|
||||
saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author,
|
||||
const std::string& coverBmpPath) {
|
||||
auto it =
|
||||
|
||||
@@ -30,9 +30,6 @@ class RecentBooksStore {
|
||||
void updateBook(const std::string& path, const std::string& title, const std::string& author,
|
||||
const std::string& coverBmpPath);
|
||||
|
||||
// Remove a book from the recent list by path
|
||||
void removeBook(const std::string& path);
|
||||
|
||||
// Get the list of recent books (most recent first)
|
||||
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
||||
|
||||
|
||||
@@ -1,141 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "activities/settings/SettingsActivity.h"
|
||||
|
||||
// Compile-time table of available font families and their enum values.
|
||||
// Used by the DynamicEnum getter/setter to map between list indices and stored FONT_FAMILY values.
|
||||
struct FontFamilyMapping {
|
||||
const char* name;
|
||||
uint8_t value;
|
||||
};
|
||||
inline constexpr FontFamilyMapping kFontFamilyMappings[] = {
|
||||
#ifndef OMIT_BOOKERLY
|
||||
{"Bookerly", CrossPointSettings::BOOKERLY},
|
||||
#endif
|
||||
#ifndef OMIT_NOTOSANS
|
||||
{"Noto Sans", CrossPointSettings::NOTOSANS},
|
||||
#endif
|
||||
#ifndef OMIT_OPENDYSLEXIC
|
||||
{"Open Dyslexic", CrossPointSettings::OPENDYSLEXIC},
|
||||
#endif
|
||||
};
|
||||
inline constexpr size_t kFontFamilyMappingCount = sizeof(kFontFamilyMappings) / sizeof(kFontFamilyMappings[0]);
|
||||
static_assert(kFontFamilyMappingCount > 0, "At least one font family must be available");
|
||||
|
||||
// Shared settings list used by both the device settings UI and the web settings API.
|
||||
// Each entry has a key (for JSON API) and category (for grouping).
|
||||
// ACTION-type entries and entries without a key are device-only.
|
||||
inline std::vector<SettingInfo> getSettingsList() {
|
||||
// Build font family options from the compile-time mapping table
|
||||
std::vector<std::string> fontFamilyOptions;
|
||||
for (size_t i = 0; i < kFontFamilyMappingCount; i++) {
|
||||
fontFamilyOptions.push_back(kFontFamilyMappings[i].name);
|
||||
}
|
||||
|
||||
return {
|
||||
// --- Display ---
|
||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen,
|
||||
{"Dark", "Light", "Custom", "Cover", "None", "Cover + Custom"}, "sleepScreen", "Display"),
|
||||
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"},
|
||||
"sleepScreenCoverMode", "Display"),
|
||||
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
|
||||
{"None", "Contrast", "Inverted"}, "sleepScreenCoverFilter", "Display"),
|
||||
SettingInfo::Enum("Letterbox Fill", &CrossPointSettings::sleepScreenLetterboxFill,
|
||||
{"Dithered", "Solid", "None"}, "sleepScreenLetterboxFill", "Display"),
|
||||
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
|
||||
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT,
|
||||
StrId::STR_COVER_CUSTOM},
|
||||
"sleepScreen", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
|
||||
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
|
||||
{StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
|
||||
"sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(
|
||||
"Status Bar", &CrossPointSettings::statusBar,
|
||||
{"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"},
|
||||
"statusBar", "Display"),
|
||||
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"},
|
||||
"hideBatteryPercentage", "Display"),
|
||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}, "refreshFrequency", "Display"),
|
||||
SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Lyra"}, "uiTheme", "Display"),
|
||||
SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix, "fadingFix", "Display"),
|
||||
StrId::STR_STATUS_BAR, &CrossPointSettings::statusBar,
|
||||
{StrId::STR_NONE_OPT, StrId::STR_NO_PROGRESS, StrId::STR_STATUS_BAR_FULL_PERCENT,
|
||||
StrId::STR_STATUS_BAR_FULL_BOOK, StrId::STR_STATUS_BAR_BOOK_ONLY, StrId::STR_STATUS_BAR_FULL_CHAPTER},
|
||||
"statusBar", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
|
||||
{StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
|
||||
StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(
|
||||
StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency,
|
||||
{StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30},
|
||||
"refreshFrequency", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme,
|
||||
{StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix",
|
||||
StrId::STR_CAT_DISPLAY),
|
||||
|
||||
// --- Reader ---
|
||||
SettingInfo::DynamicEnum(
|
||||
"Font Family", std::move(fontFamilyOptions),
|
||||
[]() -> uint8_t {
|
||||
for (uint8_t i = 0; i < kFontFamilyMappingCount; i++) {
|
||||
if (kFontFamilyMappings[i].value == SETTINGS.fontFamily) return i;
|
||||
}
|
||||
return 0; // fallback to first available family
|
||||
},
|
||||
[](uint8_t idx) {
|
||||
if (idx < kFontFamilyMappingCount) {
|
||||
SETTINGS.fontFamily = kFontFamilyMappings[idx].value;
|
||||
}
|
||||
},
|
||||
"fontFamily", "Reader"),
|
||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}, "fontSize",
|
||||
"Reader"),
|
||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}, "lineSpacing",
|
||||
"Reader"),
|
||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", "Reader"),
|
||||
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
||||
{"Justify", "Left", "Center", "Right", "Book's Style"}, "paragraphAlignment", "Reader"),
|
||||
SettingInfo::Toggle("Book's Embedded Style", &CrossPointSettings::embeddedStyle, "embeddedStyle", "Reader"),
|
||||
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", "Reader"),
|
||||
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
||||
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}, "orientation", "Reader"),
|
||||
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing,
|
||||
"extraParagraphSpacing", "Reader"),
|
||||
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing, "textAntiAliasing", "Reader"),
|
||||
SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,
|
||||
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize,
|
||||
{StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing,
|
||||
{StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER),
|
||||
SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Enum(StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment,
|
||||
{StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT,
|
||||
StrId::STR_BOOK_S_STYLE},
|
||||
"paragraphAlignment", StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
||||
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
|
||||
"orientation", StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
||||
StrId::STR_CAT_READER),
|
||||
|
||||
// --- Controls ---
|
||||
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
||||
{"Prev, Next", "Next, Prev"}, "sideButtonLayout", "Controls"),
|
||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
|
||||
"Controls"),
|
||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"},
|
||||
"shortPwrBtn", "Controls"),
|
||||
SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
|
||||
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),
|
||||
SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
|
||||
StrId::STR_CAT_CONTROLS),
|
||||
SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
|
||||
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN}, "shortPwrBtn",
|
||||
StrId::STR_CAT_CONTROLS),
|
||||
|
||||
// --- System ---
|
||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}, "sleepTimeout", "System"),
|
||||
SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout,
|
||||
{StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30},
|
||||
"sleepTimeout", StrId::STR_CAT_SYSTEM),
|
||||
|
||||
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
|
||||
SettingInfo::DynamicString(
|
||||
"KOReader Username", [] { return KOREADER_STORE.getUsername(); },
|
||||
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
|
||||
[](const std::string& v) {
|
||||
KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword());
|
||||
KOREADER_STORE.saveToFile();
|
||||
},
|
||||
"koUsername", "KOReader Sync"),
|
||||
"koUsername", StrId::STR_KOREADER_SYNC),
|
||||
SettingInfo::DynamicString(
|
||||
"KOReader Password", [] { return KOREADER_STORE.getPassword(); },
|
||||
StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); },
|
||||
[](const std::string& v) {
|
||||
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v);
|
||||
KOREADER_STORE.saveToFile();
|
||||
},
|
||||
"koPassword", "KOReader Sync"),
|
||||
"koPassword", StrId::STR_KOREADER_SYNC),
|
||||
SettingInfo::DynamicString(
|
||||
"Sync Server URL", [] { return KOREADER_STORE.getServerUrl(); },
|
||||
StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); },
|
||||
[](const std::string& v) {
|
||||
KOREADER_STORE.setServerUrl(v);
|
||||
KOREADER_STORE.saveToFile();
|
||||
},
|
||||
"koServerUrl", "KOReader Sync"),
|
||||
"koServerUrl", StrId::STR_KOREADER_SYNC),
|
||||
SettingInfo::DynamicEnum(
|
||||
"Document Matching", {"Filename", "Binary"},
|
||||
StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
|
||||
[] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
|
||||
[](uint8_t v) {
|
||||
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
|
||||
KOREADER_STORE.saveToFile();
|
||||
},
|
||||
"koMatchMethod", "KOReader Sync"),
|
||||
"koMatchMethod", StrId::STR_KOREADER_SYNC),
|
||||
|
||||
// --- OPDS Browser (web-only, uses CrossPointSettings char arrays) ---
|
||||
SettingInfo::String("OPDS Server URL", SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), "opdsServerUrl",
|
||||
"OPDS Browser"),
|
||||
SettingInfo::String("OPDS Username", SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername",
|
||||
"OPDS Browser"),
|
||||
SettingInfo::String("OPDS Password", SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword",
|
||||
"OPDS Browser"),
|
||||
SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl),
|
||||
"opdsServerUrl", StrId::STR_OPDS_BROWSER),
|
||||
SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername",
|
||||
StrId::STR_OPDS_BROWSER),
|
||||
SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword",
|
||||
StrId::STR_OPDS_BROWSER),
|
||||
};
|
||||
}
|
||||
58
src/activities/Activity.cpp
Normal file
58
src/activities/Activity.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "Activity.h"
|
||||
|
||||
void Activity::renderTaskTrampoline(void* param) {
|
||||
auto* self = static_cast<Activity*>(param);
|
||||
self->renderTaskLoop();
|
||||
}
|
||||
|
||||
void Activity::renderTaskLoop() {
|
||||
while (true) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
render(std::move(lock));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Activity::onEnter() {
|
||||
xTaskCreate(&renderTaskTrampoline, name.c_str(),
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&renderTaskHandle // Task handle
|
||||
);
|
||||
assert(renderTaskHandle != nullptr && "Failed to create render task");
|
||||
LOG_DBG("ACT", "Entering activity: %s", name.c_str());
|
||||
}
|
||||
|
||||
void Activity::onExit() {
|
||||
RenderLock lock(*this); // Ensure we don't delete the task while it's rendering
|
||||
if (renderTaskHandle) {
|
||||
vTaskDelete(renderTaskHandle);
|
||||
renderTaskHandle = nullptr;
|
||||
}
|
||||
|
||||
LOG_DBG("ACT", "Exiting activity: %s", name.c_str());
|
||||
}
|
||||
|
||||
void Activity::requestUpdate() {
|
||||
// Using direct notification to signal the render task to update
|
||||
// Increment counter so multiple rapid calls won't be lost
|
||||
if (renderTaskHandle) {
|
||||
xTaskNotify(renderTaskHandle, 1, eIncrement);
|
||||
}
|
||||
}
|
||||
|
||||
void Activity::requestUpdateAndWait() {
|
||||
// FIXME @ngxson : properly implement this using freeRTOS notification
|
||||
delay(100);
|
||||
}
|
||||
|
||||
// RenderLock
|
||||
|
||||
Activity::RenderLock::RenderLock(Activity& activity) : activity(activity) {
|
||||
xSemaphoreTake(activity.renderingMutex, portMAX_DELAY);
|
||||
}
|
||||
|
||||
Activity::RenderLock::~RenderLock() { xSemaphoreGive(activity.renderingMutex); }
|
||||
@@ -1,12 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
class MappedInputManager;
|
||||
class GfxRenderer;
|
||||
#include "GfxRenderer.h"
|
||||
#include "MappedInputManager.h"
|
||||
|
||||
class Activity {
|
||||
protected:
|
||||
@@ -14,14 +18,44 @@ class Activity {
|
||||
GfxRenderer& renderer;
|
||||
MappedInputManager& mappedInput;
|
||||
|
||||
// Task to render and display the activity
|
||||
TaskHandle_t renderTaskHandle = nullptr;
|
||||
[[noreturn]] static void renderTaskTrampoline(void* param);
|
||||
[[noreturn]] virtual void renderTaskLoop();
|
||||
|
||||
// Mutex to protect rendering operations from being deleted mid-render
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
|
||||
public:
|
||||
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
|
||||
virtual ~Activity() = default;
|
||||
virtual void onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); }
|
||||
virtual void onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); }
|
||||
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
|
||||
assert(renderingMutex != nullptr && "Failed to create rendering mutex");
|
||||
}
|
||||
virtual ~Activity() {
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
};
|
||||
class RenderLock;
|
||||
virtual void onEnter();
|
||||
virtual void onExit();
|
||||
virtual void loop() {}
|
||||
|
||||
virtual void render(RenderLock&&) {}
|
||||
virtual void requestUpdate();
|
||||
virtual void requestUpdateAndWait();
|
||||
|
||||
virtual bool skipLoopDelay() { return false; }
|
||||
virtual bool preventAutoSleep() { return false; }
|
||||
virtual bool isReaderActivity() const { return false; }
|
||||
|
||||
// RAII helper to lock rendering mutex for the duration of a scope.
|
||||
class RenderLock {
|
||||
Activity& activity;
|
||||
|
||||
public:
|
||||
explicit RenderLock(Activity& activity);
|
||||
RenderLock(const RenderLock&) = delete;
|
||||
RenderLock& operator=(const RenderLock&) = delete;
|
||||
~RenderLock();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
#include "ActivityWithSubactivity.h"
|
||||
|
||||
void ActivityWithSubactivity::renderTaskLoop() {
|
||||
while (true) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
if (!subActivity) {
|
||||
render(std::move(lock));
|
||||
}
|
||||
// If subActivity is set, consume the notification but skip parent render
|
||||
// Note: the sub-activity will call its render() from its own display task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::exitActivity() {
|
||||
// No need to lock, since onExit() already acquires its own lock
|
||||
if (subActivity) {
|
||||
LOG_DBG("ACT", "Exiting subactivity...");
|
||||
subActivity->onExit();
|
||||
subActivity.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
|
||||
// Acquire lock to avoid 2 activities rendering at the same time during transition
|
||||
RenderLock lock(*this);
|
||||
subActivity.reset(activity);
|
||||
subActivity->onEnter();
|
||||
}
|
||||
@@ -18,7 +36,15 @@ void ActivityWithSubactivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::onExit() {
|
||||
Activity::onExit();
|
||||
exitActivity();
|
||||
void ActivityWithSubactivity::requestUpdate() {
|
||||
if (!subActivity) {
|
||||
Activity::requestUpdate();
|
||||
}
|
||||
// Sub-activity should call their own requestUpdate() from their loop() function
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::onExit() {
|
||||
// No need to lock, onExit() already acquires its own lock
|
||||
exitActivity();
|
||||
Activity::onExit();
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ class ActivityWithSubactivity : public Activity {
|
||||
std::unique_ptr<Activity> subActivity = nullptr;
|
||||
void exitActivity();
|
||||
void enterNewActivity(Activity* activity);
|
||||
[[noreturn]] void renderTaskLoop() override;
|
||||
|
||||
public:
|
||||
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity(std::move(name), renderer, mappedInput) {}
|
||||
void loop() override;
|
||||
// Note: when a subactivity is active, parent requestUpdate() calls are ignored;
|
||||
// the subactivity should request its own renders. This pauses parent rendering until exit.
|
||||
void requestUpdate() override;
|
||||
void onExit() override;
|
||||
bool preventAutoSleep() override { return subActivity && subActivity->preventAutoSleep(); }
|
||||
bool skipLoopDelay() override { return subActivity && subActivity->skipLoopDelay(); }
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "BootActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "fontIds.h"
|
||||
#include "images/Logo120.h"
|
||||
@@ -13,8 +14,8 @@ void BootActivity::onEnter() {
|
||||
|
||||
renderer.clearScreen();
|
||||
renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, tr(STR_CROSSPOINT), true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, tr(STR_BOOTING));
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
@@ -3,350 +3,20 @@
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <PlaceholderCoverGenerator.h>
|
||||
#include <Serialization.h>
|
||||
#include <I18n.h>
|
||||
#include <Txt.h>
|
||||
#include <Xtc.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "util/BookSettings.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "images/Logo120.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Number of source pixels along the image edge to average for the dominant color
|
||||
constexpr int EDGE_SAMPLE_DEPTH = 20;
|
||||
|
||||
// Map a 2-bit quantized pixel value to an 8-bit grayscale value
|
||||
constexpr uint8_t val2bitToGray(uint8_t val2bit) { return val2bit * 85; }
|
||||
|
||||
// Letterbox fill data: one average gray value per edge (top/bottom or left/right).
|
||||
struct LetterboxFillData {
|
||||
uint8_t avgA = 128; // average gray of edge A (top or left)
|
||||
uint8_t avgB = 128; // average gray of edge B (bottom or right)
|
||||
int letterboxA = 0; // pixel size of the first letterbox area (top or left)
|
||||
int letterboxB = 0; // pixel size of the second letterbox area (bottom or right)
|
||||
bool horizontal = false; // true = top/bottom letterbox, false = left/right
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// Snap an 8-bit gray value to the nearest of the 4 e-ink levels: 0, 85, 170, 255.
|
||||
uint8_t snapToEinkLevel(uint8_t gray) {
|
||||
// Thresholds at midpoints: 42, 127, 212
|
||||
if (gray < 43) return 0;
|
||||
if (gray < 128) return 85;
|
||||
if (gray < 213) return 170;
|
||||
return 255;
|
||||
}
|
||||
|
||||
// 4x4 Bayer ordered dithering matrix, values 0-255.
|
||||
// Produces a structured halftone pattern for 4-level quantization.
|
||||
// clang-format off
|
||||
constexpr uint8_t BAYER_4X4[4][4] = {
|
||||
{ 0, 128, 32, 160},
|
||||
{192, 64, 224, 96},
|
||||
{ 48, 176, 16, 144},
|
||||
{240, 112, 208, 80}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// Ordered (Bayer) dithering for 4-level e-ink display.
|
||||
// Maps an 8-bit gray value to a 2-bit level (0-3) using the Bayer matrix
|
||||
// to produce a structured, repeating halftone pattern.
|
||||
uint8_t quantizeBayerDither(int gray, int x, int y) {
|
||||
const int threshold = BAYER_4X4[y & 3][x & 3];
|
||||
const int scaled = gray * 3;
|
||||
if (scaled < 255) {
|
||||
return (scaled + threshold >= 255) ? 1 : 0;
|
||||
} else if (scaled < 510) {
|
||||
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
||||
} else {
|
||||
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether a gray value would produce a dithered mix that crosses the
|
||||
// level-2 / level-3 boundary. This is the ONLY boundary where some dithered
|
||||
// pixels are BLACK (level ≤ 2) and others are WHITE (level 3) in the BW pass,
|
||||
// creating a high-frequency checkerboard that causes e-ink display crosstalk
|
||||
// and washes out adjacent content during HALF_REFRESH.
|
||||
// Gray values 171-254 produce a level-2/level-3 mix via Bayer dithering.
|
||||
bool bayerCrossesBwBoundary(uint8_t gray) {
|
||||
return gray > 170 && gray < 255;
|
||||
}
|
||||
|
||||
// Hash-based block dithering for BW-boundary gray values (171-254).
|
||||
// Each blockSize×blockSize pixel block gets a single uniform level (2 or 3),
|
||||
// determined by a deterministic spatial hash. The proportion of level-3 blocks
|
||||
// approximates the target gray. Unlike Bayer, the pattern is irregular
|
||||
// (noise-like), making it much less visually obvious at the same block size.
|
||||
// The hash is purely spatial (depends only on x, y, blockSize) so it produces
|
||||
// identical levels across BW, LSB, and MSB render passes.
|
||||
static constexpr int BW_DITHER_BLOCK = 2;
|
||||
|
||||
uint8_t hashBlockDither(uint8_t avg, int x, int y) {
|
||||
const int bx = x / BW_DITHER_BLOCK;
|
||||
const int by = y / BW_DITHER_BLOCK;
|
||||
// Fast mixing hash (splitmix32-inspired)
|
||||
uint32_t h = (uint32_t)bx * 2654435761u ^ (uint32_t)by * 2246822519u;
|
||||
h ^= h >> 16;
|
||||
h *= 0x45d9f3bu;
|
||||
h ^= h >> 16;
|
||||
// Proportion of level-3 blocks needed to approximate the target gray
|
||||
const float ratio = (avg - 170.0f) / 85.0f;
|
||||
const uint32_t threshold = (uint32_t)(ratio * 4294967295.0f);
|
||||
return (h < threshold) ? 3 : 2;
|
||||
}
|
||||
|
||||
// --- Edge average cache ---
|
||||
// Caches the computed edge averages alongside the cover BMP so we don't rescan on every sleep.
|
||||
constexpr uint8_t EDGE_CACHE_VERSION = 2;
|
||||
|
||||
bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxFillData& data) {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("SLP", path, file)) return false;
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(file, version);
|
||||
if (version != EDGE_CACHE_VERSION) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedW, cachedH;
|
||||
serialization::readPod(file, cachedW);
|
||||
serialization::readPod(file, cachedH);
|
||||
if (cachedW != static_cast<uint16_t>(screenWidth) || cachedH != static_cast<uint16_t>(screenHeight)) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t horizontal;
|
||||
serialization::readPod(file, horizontal);
|
||||
data.horizontal = (horizontal != 0);
|
||||
|
||||
serialization::readPod(file, data.avgA);
|
||||
serialization::readPod(file, data.avgB);
|
||||
|
||||
int16_t lbA, lbB;
|
||||
serialization::readPod(file, lbA);
|
||||
serialization::readPod(file, lbB);
|
||||
data.letterboxA = lbA;
|
||||
data.letterboxB = lbB;
|
||||
|
||||
file.close();
|
||||
data.valid = true;
|
||||
LOG_DBG("SLP", "Loaded edge cache from %s (avgA=%d, avgB=%d)", path.c_str(), data.avgA, data.avgB);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxFillData& data) {
|
||||
if (!data.valid) return false;
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("SLP", path, file)) return false;
|
||||
|
||||
serialization::writePod(file, EDGE_CACHE_VERSION);
|
||||
serialization::writePod(file, static_cast<uint16_t>(screenWidth));
|
||||
serialization::writePod(file, static_cast<uint16_t>(screenHeight));
|
||||
serialization::writePod(file, static_cast<uint8_t>(data.horizontal ? 1 : 0));
|
||||
serialization::writePod(file, data.avgA);
|
||||
serialization::writePod(file, data.avgB);
|
||||
serialization::writePod(file, static_cast<int16_t>(data.letterboxA));
|
||||
serialization::writePod(file, static_cast<int16_t>(data.letterboxB));
|
||||
file.close();
|
||||
|
||||
LOG_DBG("SLP", "Saved edge cache to %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read the bitmap once to compute a single average gray value for the top/bottom or left/right edges.
|
||||
// Only computes running sums -- no per-pixel arrays, no malloc beyond row buffers.
|
||||
// After sampling the bitmap is rewound via rewindToData().
|
||||
LetterboxFillData computeEdgeAverages(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight,
|
||||
float scale, float cropX, float cropY) {
|
||||
LetterboxFillData data;
|
||||
|
||||
const int cropPixX = static_cast<int>(std::floor(bitmap.getWidth() * cropX / 2.0f));
|
||||
const int cropPixY = static_cast<int>(std::floor(bitmap.getHeight() * cropY / 2.0f));
|
||||
const int visibleWidth = bitmap.getWidth() - 2 * cropPixX;
|
||||
const int visibleHeight = bitmap.getHeight() - 2 * cropPixY;
|
||||
|
||||
if (visibleWidth <= 0 || visibleHeight <= 0) return data;
|
||||
|
||||
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
|
||||
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||
if (!outputRow || !rowBytes) {
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return data;
|
||||
}
|
||||
|
||||
if (imgY > 0) {
|
||||
// Top/bottom letterboxing -- compute overall average of first/last EDGE_SAMPLE_DEPTH rows
|
||||
data.horizontal = true;
|
||||
const int scaledHeight = static_cast<int>(std::round(static_cast<float>(visibleHeight) * scale));
|
||||
data.letterboxA = imgY;
|
||||
data.letterboxB = pageHeight - imgY - scaledHeight;
|
||||
if (data.letterboxB < 0) data.letterboxB = 0;
|
||||
|
||||
const int sampleRows = std::min(EDGE_SAMPLE_DEPTH, visibleHeight);
|
||||
uint64_t sumTop = 0, sumBot = 0;
|
||||
int countTop = 0, countBot = 0;
|
||||
|
||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break;
|
||||
const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||
if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue;
|
||||
const int outY = logicalY - cropPixY;
|
||||
|
||||
const bool inTop = (outY < sampleRows);
|
||||
const bool inBot = (outY >= visibleHeight - sampleRows);
|
||||
if (!inTop && !inBot) continue;
|
||||
|
||||
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
||||
const uint8_t gray = val2bitToGray(val);
|
||||
if (inTop) {
|
||||
sumTop += gray;
|
||||
countTop++;
|
||||
}
|
||||
if (inBot) {
|
||||
sumBot += gray;
|
||||
countBot++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.avgA = countTop > 0 ? static_cast<uint8_t>(sumTop / countTop) : 128;
|
||||
data.avgB = countBot > 0 ? static_cast<uint8_t>(sumBot / countBot) : 128;
|
||||
data.valid = true;
|
||||
} else if (imgX > 0) {
|
||||
// Left/right letterboxing -- compute overall average of first/last EDGE_SAMPLE_DEPTH columns
|
||||
data.horizontal = false;
|
||||
const int scaledWidth = static_cast<int>(std::round(static_cast<float>(visibleWidth) * scale));
|
||||
data.letterboxA = imgX;
|
||||
data.letterboxB = pageWidth - imgX - scaledWidth;
|
||||
if (data.letterboxB < 0) data.letterboxB = 0;
|
||||
|
||||
const int sampleCols = std::min(EDGE_SAMPLE_DEPTH, visibleWidth);
|
||||
uint64_t sumLeft = 0, sumRight = 0;
|
||||
int countLeft = 0, countRight = 0;
|
||||
|
||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break;
|
||||
const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
||||
if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue;
|
||||
|
||||
for (int bmpX = cropPixX; bmpX < cropPixX + sampleCols; bmpX++) {
|
||||
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
||||
sumLeft += val2bitToGray(val);
|
||||
countLeft++;
|
||||
}
|
||||
for (int bmpX = bitmap.getWidth() - cropPixX - sampleCols; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
||||
sumRight += val2bitToGray(val);
|
||||
countRight++;
|
||||
}
|
||||
}
|
||||
|
||||
data.avgA = countLeft > 0 ? static_cast<uint8_t>(sumLeft / countLeft) : 128;
|
||||
data.avgB = countRight > 0 ? static_cast<uint8_t>(sumRight / countRight) : 128;
|
||||
data.valid = true;
|
||||
}
|
||||
|
||||
bitmap.rewindToData();
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Draw letterbox fill in the areas around the cover image.
|
||||
// DITHERED: fills with the edge average using Bayer ordered dithering to approximate the color.
|
||||
// SOLID: snaps edge average to nearest e-ink level (0/85/170/255) for a clean uniform fill.
|
||||
// Must be called once per render pass (BW, GRAYSCALE_LSB, GRAYSCALE_MSB).
|
||||
void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uint8_t fillMode) {
|
||||
if (!data.valid) return;
|
||||
|
||||
const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID);
|
||||
|
||||
// For DITHERED mode with gray values in 171-254 (the level-2/level-3 BW boundary):
|
||||
// Pixel-level Bayer dithering creates a regular high-frequency checkerboard in
|
||||
// the BW pass that causes e-ink display crosstalk during HALF_REFRESH.
|
||||
//
|
||||
// Solution: HASH-BASED BLOCK DITHERING. Each 2x2 pixel block gets a single
|
||||
// level (2 or 3) determined by a spatial hash, with the proportion of level-3
|
||||
// blocks tuned to approximate the target gray. The 2px minimum run avoids BW
|
||||
// crosstalk, and the irregular hash pattern is much less visible than a regular
|
||||
// Bayer grid at the same block size.
|
||||
const bool hashA = !isSolid && bayerCrossesBwBoundary(data.avgA);
|
||||
const bool hashB = !isSolid && bayerCrossesBwBoundary(data.avgB);
|
||||
|
||||
// For solid mode: snap to nearest e-ink level
|
||||
const uint8_t levelA = isSolid ? snapToEinkLevel(data.avgA) / 85 : 0;
|
||||
const uint8_t levelB = isSolid ? snapToEinkLevel(data.avgB) / 85 : 0;
|
||||
|
||||
if (data.horizontal) {
|
||||
if (data.letterboxA > 0) {
|
||||
for (int y = 0; y < data.letterboxA; y++)
|
||||
for (int x = 0; x < renderer.getScreenWidth(); x++) {
|
||||
uint8_t lv;
|
||||
if (isSolid) lv = levelA;
|
||||
else if (hashA) lv = hashBlockDither(data.avgA, x, y);
|
||||
else lv = quantizeBayerDither(data.avgA, x, y);
|
||||
renderer.drawPixelGray(x, y, lv);
|
||||
}
|
||||
}
|
||||
if (data.letterboxB > 0) {
|
||||
const int start = renderer.getScreenHeight() - data.letterboxB;
|
||||
for (int y = start; y < renderer.getScreenHeight(); y++)
|
||||
for (int x = 0; x < renderer.getScreenWidth(); x++) {
|
||||
uint8_t lv;
|
||||
if (isSolid) lv = levelB;
|
||||
else if (hashB) lv = hashBlockDither(data.avgB, x, y);
|
||||
else lv = quantizeBayerDither(data.avgB, x, y);
|
||||
renderer.drawPixelGray(x, y, lv);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (data.letterboxA > 0) {
|
||||
for (int x = 0; x < data.letterboxA; x++)
|
||||
for (int y = 0; y < renderer.getScreenHeight(); y++) {
|
||||
uint8_t lv;
|
||||
if (isSolid) lv = levelA;
|
||||
else if (hashA) lv = hashBlockDither(data.avgA, x, y);
|
||||
else lv = quantizeBayerDither(data.avgA, x, y);
|
||||
renderer.drawPixelGray(x, y, lv);
|
||||
}
|
||||
}
|
||||
if (data.letterboxB > 0) {
|
||||
const int start = renderer.getScreenWidth() - data.letterboxB;
|
||||
for (int x = start; x < renderer.getScreenWidth(); x++)
|
||||
for (int y = 0; y < renderer.getScreenHeight(); y++) {
|
||||
uint8_t lv;
|
||||
if (isSolid) lv = levelB;
|
||||
else if (hashB) lv = hashBlockDither(data.avgB, x, y);
|
||||
else lv = quantizeBayerDither(data.avgB, x, y);
|
||||
renderer.drawPixelGray(x, y, lv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SleepActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
GUI.drawPopup(renderer, "Entering Sleep...");
|
||||
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
|
||||
|
||||
switch (SETTINGS.sleepScreen) {
|
||||
case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK):
|
||||
@@ -441,8 +111,8 @@ void SleepActivity::renderDefaultSleepScreen() const {
|
||||
|
||||
renderer.clearScreen();
|
||||
renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, tr(STR_CROSSPOINT), true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, tr(STR_SLEEPING));
|
||||
|
||||
// Make sleep screen dark unless light is selected in settings
|
||||
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) {
|
||||
@@ -452,22 +122,21 @@ void SleepActivity::renderDefaultSleepScreen() const {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
}
|
||||
|
||||
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath,
|
||||
uint8_t fillModeOverride) const {
|
||||
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
||||
int x, y;
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
float cropX = 0, cropY = 0;
|
||||
|
||||
LOG_DBG("SLP", "bitmap %d x %d, screen %d x %d", bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight);
|
||||
|
||||
// Always compute aspect-ratio-preserving scale and position (supports both larger and smaller images)
|
||||
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
||||
// image will scale, make sure placement is right
|
||||
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
||||
|
||||
LOG_DBG("SLP", "bitmap ratio: %f, screen ratio: %f", ratio, screenRatio);
|
||||
if (ratio > screenRatio) {
|
||||
// image wider than viewport ratio, needs to be centered vertically
|
||||
// image wider than viewport ratio, scaled down image needs to be centered vertically
|
||||
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||
cropX = 1.0f - (screenRatio / ratio);
|
||||
LOG_DBG("SLP", "Cropping bitmap x: %f", cropX);
|
||||
@@ -477,7 +146,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
||||
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
||||
LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y);
|
||||
} else {
|
||||
// image taller than or equal to viewport ratio, needs to be centered horizontally
|
||||
// image taller than viewport ratio, scaled down image needs to be centered horizontally
|
||||
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||
cropY = 1.0f - (ratio / screenRatio);
|
||||
LOG_DBG("SLP", "Cropping bitmap y: %f", cropY);
|
||||
@@ -487,57 +156,18 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
||||
y = 0;
|
||||
LOG_DBG("SLP", "Centering with ratio %f to x=%d", ratio, x);
|
||||
}
|
||||
} else {
|
||||
// center the image
|
||||
x = (pageWidth - bitmap.getWidth()) / 2;
|
||||
y = (pageHeight - bitmap.getHeight()) / 2;
|
||||
}
|
||||
|
||||
LOG_DBG("SLP", "drawing to %d x %d", x, y);
|
||||
|
||||
// Compute the scale factor (same formula as drawBitmap) so we can map screen coords to source coords
|
||||
const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth();
|
||||
const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight();
|
||||
const float scale =
|
||||
std::min(static_cast<float>(pageWidth) / effectiveWidth, static_cast<float>(pageHeight) / effectiveHeight);
|
||||
|
||||
// Determine letterbox fill settings (per-book override takes precedence)
|
||||
const uint8_t fillMode = (fillModeOverride != BookSettings::USE_GLOBAL &&
|
||||
fillModeOverride < CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL_COUNT)
|
||||
? fillModeOverride
|
||||
: SETTINGS.sleepScreenLetterboxFill;
|
||||
const bool wantFill = (fillMode != CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_NONE);
|
||||
|
||||
static const char* fillModeNames[] = {"dithered", "solid", "none"};
|
||||
const char* fillModeName = (fillMode < 3) ? fillModeNames[fillMode] : "unknown";
|
||||
|
||||
// Compute edge averages if letterbox fill is requested (try cache first)
|
||||
LetterboxFillData fillData;
|
||||
const bool hasLetterbox = (x > 0 || y > 0);
|
||||
if (hasLetterbox && wantFill) {
|
||||
bool cacheLoaded = false;
|
||||
if (!edgeCachePath.empty()) {
|
||||
cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, fillData);
|
||||
}
|
||||
if (!cacheLoaded) {
|
||||
LOG_DBG("SLP", "Letterbox detected (x=%d, y=%d), computing edge averages for %s fill", x, y, fillModeName);
|
||||
fillData = computeEdgeAverages(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY);
|
||||
if (fillData.valid && !edgeCachePath.empty()) {
|
||||
saveEdgeCache(edgeCachePath, pageWidth, pageHeight, fillData);
|
||||
}
|
||||
}
|
||||
if (fillData.valid) {
|
||||
LOG_DBG("SLP", "Letterbox fill: %s, horizontal=%d, avgA=%d, avgB=%d, letterboxA=%d, letterboxB=%d",
|
||||
fillModeName, fillData.horizontal, fillData.avgA, fillData.avgB, fillData.letterboxA,
|
||||
fillData.letterboxB);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
||||
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
|
||||
|
||||
// Draw letterbox fill (BW pass)
|
||||
if (fillData.valid) {
|
||||
drawLetterboxFill(renderer, fillData, fillMode);
|
||||
}
|
||||
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||
|
||||
if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) {
|
||||
@@ -550,18 +180,12 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
||||
bitmap.rewindToData();
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
if (fillData.valid) {
|
||||
drawLetterboxFill(renderer, fillData, fillMode);
|
||||
}
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
bitmap.rewindToData();
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
if (fillData.valid) {
|
||||
drawLetterboxFill(renderer, fillData, fillMode);
|
||||
}
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
@@ -586,7 +210,6 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
}
|
||||
|
||||
std::string coverBmpPath;
|
||||
std::string bookCachePath;
|
||||
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
|
||||
|
||||
// Check if the current book is XTC, TXT, or EPUB
|
||||
@@ -600,17 +223,11 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
}
|
||||
|
||||
if (!lastXtc.generateCoverBmp()) {
|
||||
LOG_DBG("SLP", "XTC cover generation failed, trying placeholder");
|
||||
PlaceholderCoverGenerator::generate(lastXtc.getCoverBmpPath(), lastXtc.getTitle(), lastXtc.getAuthor(), 480, 800);
|
||||
}
|
||||
|
||||
if (!Storage.exists(lastXtc.getCoverBmpPath().c_str())) {
|
||||
LOG_ERR("SLP", "Failed to generate XTC cover bmp");
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
coverBmpPath = lastXtc.getCoverBmpPath();
|
||||
bookCachePath = lastXtc.getCachePath();
|
||||
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) {
|
||||
// Handle TXT file - looks for cover image in the same folder
|
||||
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
@@ -620,17 +237,11 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
}
|
||||
|
||||
if (!lastTxt.generateCoverBmp()) {
|
||||
LOG_DBG("SLP", "TXT cover generation failed, trying placeholder");
|
||||
PlaceholderCoverGenerator::generate(lastTxt.getCoverBmpPath(), lastTxt.getTitle(), "", 480, 800);
|
||||
}
|
||||
|
||||
if (!Storage.exists(lastTxt.getCoverBmpPath().c_str())) {
|
||||
LOG_ERR("SLP", "No cover image found for TXT file");
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
coverBmpPath = lastTxt.getCoverBmpPath();
|
||||
bookCachePath = lastTxt.getCachePath();
|
||||
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
|
||||
// Handle EPUB file
|
||||
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
@@ -641,44 +252,21 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
}
|
||||
|
||||
if (!lastEpub.generateCoverBmp(cropped)) {
|
||||
LOG_DBG("SLP", "EPUB cover generation failed, trying placeholder");
|
||||
if (!PlaceholderCoverGenerator::generate(lastEpub.getCoverBmpPath(cropped), lastEpub.getTitle(),
|
||||
lastEpub.getAuthor(), 480, 800)) {
|
||||
LOG_DBG("SLP", "Placeholder generation failed, creating X-pattern marker");
|
||||
lastEpub.generateInvalidFormatCoverBmp(cropped);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Epub::isValidThumbnailBmp(lastEpub.getCoverBmpPath(cropped))) {
|
||||
LOG_ERR("SLP", "Failed to generate cover bmp");
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
coverBmpPath = lastEpub.getCoverBmpPath(cropped);
|
||||
bookCachePath = lastEpub.getCachePath();
|
||||
} else {
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
// Load per-book letterbox fill override (falls back to global if not set)
|
||||
uint8_t fillModeOverride = BookSettings::USE_GLOBAL;
|
||||
if (!bookCachePath.empty()) {
|
||||
auto bookSettings = BookSettings::load(bookCachePath);
|
||||
fillModeOverride = bookSettings.letterboxFillOverride;
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
||||
Bitmap bitmap(file);
|
||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||
LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str());
|
||||
// Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin)
|
||||
std::string edgeCachePath;
|
||||
const auto dotPos = coverBmpPath.rfind(".bmp");
|
||||
if (dotPos != std::string::npos) {
|
||||
edgeCachePath = coverBmpPath.substr(0, dotPos) + "_edges.bin";
|
||||
}
|
||||
renderBitmapSleepScreen(bitmap, edgeCachePath, fillModeOverride);
|
||||
renderBitmapSleepScreen(bitmap);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "../Activity.h"
|
||||
|
||||
class Bitmap;
|
||||
@@ -16,8 +13,6 @@ class SleepActivity final : public Activity {
|
||||
void renderDefaultSleepScreen() const;
|
||||
void renderCustomSleepScreen() const;
|
||||
void renderCoverSleepScreen() const;
|
||||
// fillModeOverride: 0xFF = use global setting, otherwise a SLEEP_SCREEN_LETTERBOX_FILL value.
|
||||
void renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath = "",
|
||||
uint8_t fillModeOverride = 0xFF) const;
|
||||
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
|
||||
void renderBlankSleepScreen() const;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <OpdsStream.h>
|
||||
#include <WiFi.h>
|
||||
@@ -19,30 +20,17 @@ namespace {
|
||||
constexpr int PAGE_ITEMS = 23;
|
||||
} // namespace
|
||||
|
||||
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<OpdsBookBrowserActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
state = BrowserState::CHECK_WIFI;
|
||||
entries.clear();
|
||||
navigationHistory.clear();
|
||||
currentPath = ""; // Root path - user provides full URL in settings
|
||||
selectorIndex = 0;
|
||||
errorMessage.clear();
|
||||
statusMessage = "Checking WiFi...";
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&OpdsBookBrowserActivity::taskTrampoline, "OpdsBookBrowserTask",
|
||||
4096, // Stack size (larger for HTTP operations)
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
statusMessage = tr(STR_CHECKING_WIFI);
|
||||
requestUpdate();
|
||||
|
||||
// Check WiFi and connect if needed, then fetch feed
|
||||
checkAndConnectWifi();
|
||||
@@ -54,13 +42,6 @@ void OpdsBookBrowserActivity::onExit() {
|
||||
// Turn off WiFi when exiting
|
||||
WiFi.mode(WIFI_OFF);
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
entries.clear();
|
||||
navigationHistory.clear();
|
||||
}
|
||||
@@ -80,8 +61,8 @@ void OpdsBookBrowserActivity::loop() {
|
||||
// WiFi connected - just retry fetching the feed
|
||||
LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch");
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
updateRequired = true;
|
||||
statusMessage = tr(STR_LOADING);
|
||||
requestUpdate();
|
||||
fetchFeed(currentPath);
|
||||
} else {
|
||||
// WiFi not connected - launch WiFi selection
|
||||
@@ -134,50 +115,38 @@ void OpdsBookBrowserActivity::loop() {
|
||||
if (!entries.empty()) {
|
||||
buttonNavigator.onNextRelease([this] {
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousRelease([this] {
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onNextContinuous([this] {
|
||||
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousContinuous([this] {
|
||||
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::render() const {
|
||||
void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
|
||||
|
||||
if (state == BrowserState::CHECK_WIFI) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
|
||||
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
@@ -185,23 +154,23 @@ void OpdsBookBrowserActivity::render() const {
|
||||
|
||||
if (state == BrowserState::LOADING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
|
||||
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == BrowserState::ERROR) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Error:");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG));
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str());
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Retry", "", "");
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == BrowserState::DOWNLOADING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, "Downloading...");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING));
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str());
|
||||
if (downloadTotal > 0) {
|
||||
const int barWidth = pageWidth - 100;
|
||||
@@ -216,15 +185,15 @@ void OpdsBookBrowserActivity::render() const {
|
||||
|
||||
// Browsing state
|
||||
// Show appropriate button hint based on selected entry type
|
||||
const char* confirmLabel = "Open";
|
||||
const char* confirmLabel = tr(STR_OPEN);
|
||||
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
|
||||
confirmLabel = "Download";
|
||||
confirmLabel = tr(STR_DOWNLOAD);
|
||||
}
|
||||
const auto labels = mappedInput.mapLabels("« Back", confirmLabel, "", "");
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
if (entries.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "No entries found");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES));
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
@@ -259,8 +228,8 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||
const char* serverUrl = SETTINGS.opdsServerUrl;
|
||||
if (strlen(serverUrl) == 0) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "No server URL configured";
|
||||
updateRequired = true;
|
||||
errorMessage = tr(STR_NO_SERVER_URL);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,16 +242,16 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||
OpdsParserStream stream{parser};
|
||||
if (!HttpDownloader::fetchUrl(url, stream)) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "Failed to fetch feed";
|
||||
updateRequired = true;
|
||||
errorMessage = tr(STR_FETCH_FEED_FAILED);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parser) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "Failed to parse feed";
|
||||
updateRequired = true;
|
||||
errorMessage = tr(STR_PARSE_FEED_FAILED);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,13 +261,13 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||
|
||||
if (entries.empty()) {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "No entries found";
|
||||
updateRequired = true;
|
||||
errorMessage = tr(STR_NO_ENTRIES);
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
state = BrowserState::BROWSING;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
||||
@@ -307,10 +276,10 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
||||
currentPath = entry.href;
|
||||
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
statusMessage = tr(STR_LOADING);
|
||||
entries.clear();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
|
||||
fetchFeed(currentPath);
|
||||
}
|
||||
@@ -325,10 +294,10 @@ void OpdsBookBrowserActivity::navigateBack() {
|
||||
navigationHistory.pop_back();
|
||||
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
statusMessage = tr(STR_LOADING);
|
||||
entries.clear();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
|
||||
fetchFeed(currentPath);
|
||||
}
|
||||
@@ -339,7 +308,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
statusMessage = book.title;
|
||||
downloadProgress = 0;
|
||||
downloadTotal = 0;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
|
||||
// Build full download URL
|
||||
std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
|
||||
@@ -357,7 +326,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
||||
downloadProgress = downloaded;
|
||||
downloadTotal = total;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
if (result == HttpDownloader::OK) {
|
||||
@@ -369,11 +338,11 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str());
|
||||
|
||||
state = BrowserState::BROWSING;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
} else {
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "Download failed";
|
||||
updateRequired = true;
|
||||
errorMessage = tr(STR_DOWNLOAD_FAILED);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,8 +350,8 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
||||
// Already connected? Verify connection is valid by checking IP
|
||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
updateRequired = true;
|
||||
statusMessage = tr(STR_LOADING);
|
||||
requestUpdate();
|
||||
fetchFeed(currentPath);
|
||||
return;
|
||||
}
|
||||
@@ -393,7 +362,7 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
||||
|
||||
void OpdsBookBrowserActivity::launchWifiSelection() {
|
||||
state = BrowserState::WIFI_SELECTION;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
@@ -405,8 +374,8 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
||||
if (connected) {
|
||||
LOG_DBG("OPDS", "WiFi connected via selection, fetching feed");
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
updateRequired = true;
|
||||
statusMessage = tr(STR_LOADING);
|
||||
requestUpdate();
|
||||
fetchFeed(currentPath);
|
||||
} else {
|
||||
LOG_DBG("OPDS", "WiFi selection cancelled/failed");
|
||||
@@ -415,7 +384,7 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
||||
WiFi.disconnect();
|
||||
WiFi.mode(WIFI_OFF);
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = "WiFi connection failed";
|
||||
updateRequired = true;
|
||||
errorMessage = tr(STR_WIFI_CONN_FAILED);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#pragma once
|
||||
#include <OpdsParser.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -34,13 +31,10 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
ButtonNavigator buttonNavigator;
|
||||
bool updateRequired = false;
|
||||
|
||||
BrowserState state = BrowserState::LOADING;
|
||||
std::vector<OpdsEntry> entries;
|
||||
std::vector<std::string> navigationHistory; // Stack of previous feed paths for back navigation
|
||||
@@ -53,10 +47,6 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
||||
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
void checkAndConnectWifi();
|
||||
void launchWifiSelection();
|
||||
void onWifiSelectionComplete(bool connected);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Utf8.h>
|
||||
#include <PlaceholderCoverGenerator.h>
|
||||
#include <Xtc.h>
|
||||
|
||||
#include <cstring>
|
||||
@@ -20,11 +20,6 @@
|
||||
#include "fontIds.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
void HomeActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<HomeActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
int count = 4; // My Library, Recents, File transfer, Settings
|
||||
if (!recentBooks.empty()) {
|
||||
@@ -65,61 +60,46 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
for (RecentBook& book : recentBooks) {
|
||||
if (!book.coverBmpPath.empty()) {
|
||||
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
|
||||
if (!Epub::isValidThumbnailBmp(coverPath)) {
|
||||
if (!showingLoading) {
|
||||
showingLoading = true;
|
||||
popupRect = GUI.drawPopup(renderer, "Loading...");
|
||||
}
|
||||
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
|
||||
|
||||
bool success = false;
|
||||
|
||||
// Try format-specific thumbnail generation first (Real Cover)
|
||||
if (!Storage.exists(coverPath.c_str())) {
|
||||
// If epub, try to load the metadata for title/author and cover
|
||||
if (StringUtils::checkFileExtension(book.path, ".epub")) {
|
||||
Epub epub(book.path, "/.crosspoint");
|
||||
// Try fast cache-only load first; only build cache if missing
|
||||
if (!epub.load(false, true)) {
|
||||
// Cache missing — build it (may take longer)
|
||||
epub.load(true, true);
|
||||
// Skip loading css since we only need metadata here
|
||||
epub.load(false, true);
|
||||
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (!showingLoading) {
|
||||
showingLoading = true;
|
||||
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING));
|
||||
}
|
||||
success = epub.generateThumbBmp(coverHeight);
|
||||
if (success) {
|
||||
const std::string thumbPath = epub.getThumbBmpPath(coverHeight);
|
||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, thumbPath);
|
||||
book.coverBmpPath = thumbPath;
|
||||
} else {
|
||||
// Fallback: generate a placeholder thumbnail with title/author
|
||||
const int thumbWidth = static_cast<int>(coverHeight * 0.6);
|
||||
success = PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight);
|
||||
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
|
||||
bool success = epub.generateThumbBmp(coverHeight);
|
||||
if (!success) {
|
||||
// Last resort: X-pattern marker to prevent repeated generation attempts
|
||||
epub.generateInvalidFormatThumbBmp(coverHeight);
|
||||
}
|
||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, "");
|
||||
book.coverBmpPath = "";
|
||||
}
|
||||
coverRendered = false;
|
||||
requestUpdate();
|
||||
} else if (StringUtils::checkFileExtension(book.path, ".xtch") ||
|
||||
StringUtils::checkFileExtension(book.path, ".xtc")) {
|
||||
// Handle XTC file
|
||||
Xtc xtc(book.path, "/.crosspoint");
|
||||
if (xtc.load()) {
|
||||
success = xtc.generateThumbBmp(coverHeight);
|
||||
if (success) {
|
||||
const std::string thumbPath = xtc.getThumbBmpPath(coverHeight);
|
||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, thumbPath);
|
||||
book.coverBmpPath = thumbPath;
|
||||
}
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (!showingLoading) {
|
||||
showingLoading = true;
|
||||
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING));
|
||||
}
|
||||
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
|
||||
bool success = xtc.generateThumbBmp(coverHeight);
|
||||
if (!success) {
|
||||
// Fallback: generate a placeholder thumbnail with title/author
|
||||
const int thumbWidth = static_cast<int>(coverHeight * 0.6);
|
||||
PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight);
|
||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, "");
|
||||
book.coverBmpPath = "";
|
||||
}
|
||||
} else {
|
||||
// Unknown format: generate a placeholder thumbnail
|
||||
const int thumbWidth = static_cast<int>(coverHeight * 0.6);
|
||||
PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight);
|
||||
}
|
||||
|
||||
coverRendered = false;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
progress++;
|
||||
@@ -132,8 +112,6 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
void HomeActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Check if OPDS browser URL is configured
|
||||
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
||||
|
||||
@@ -143,28 +121,12 @@ void HomeActivity::onEnter() {
|
||||
loadRecentBooks(metrics.homeRecentBooksCount);
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void HomeActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
|
||||
// Free the stored cover buffer if any
|
||||
freeCoverBuffer();
|
||||
}
|
||||
@@ -216,12 +178,12 @@ void HomeActivity::loop() {
|
||||
|
||||
buttonNavigator.onNext([this, menuCount] {
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this, menuCount] {
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
@@ -250,19 +212,7 @@ void HomeActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::render() {
|
||||
void HomeActivity::render(Activity::RenderLock&&) {
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
@@ -277,10 +227,11 @@ void HomeActivity::render() {
|
||||
std::bind(&HomeActivity::storeCoverBuffer, this));
|
||||
|
||||
// Build menu items dynamically
|
||||
std::vector<const char*> menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"};
|
||||
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
|
||||
tr(STR_SETTINGS_TITLE)};
|
||||
if (hasOpdsUrl) {
|
||||
// Insert OPDS Browser after My Library
|
||||
menuItems.insert(menuItems.begin() + 2, "OPDS Browser");
|
||||
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
|
||||
}
|
||||
|
||||
GUI.drawButtonMenu(
|
||||
@@ -291,14 +242,14 @@ void HomeActivity::render() {
|
||||
static_cast<int>(menuItems.size()), selectorIndex - recentBooks.size(),
|
||||
[&menuItems](int index) { return std::string(menuItems[index]); }, nullptr);
|
||||
|
||||
const auto labels = mappedInput.mapLabels("", "Select", "Up", "Down");
|
||||
const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
|
||||
if (!firstRenderDone) {
|
||||
firstRenderDone = true;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
} else if (!recentsLoaded && !recentsLoading) {
|
||||
recentsLoading = true;
|
||||
loadRecentCovers(metrics.homeCoverHeight);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
@@ -14,11 +10,8 @@ struct RecentBook;
|
||||
struct Rect;
|
||||
|
||||
class HomeActivity final : public Activity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
ButtonNavigator buttonNavigator;
|
||||
int selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
bool recentsLoading = false;
|
||||
bool recentsLoaded = false;
|
||||
bool firstRenderDone = false;
|
||||
@@ -34,9 +27,6 @@ class HomeActivity final : public Activity {
|
||||
const std::function<void()> onFileTransferOpen;
|
||||
const std::function<void()> onOpdsBrowserOpen;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
int getMenuItemCount() const;
|
||||
bool storeCoverBuffer(); // Store frame buffer for cover image
|
||||
bool restoreCoverBuffer(); // Restore frame buffer from stored cover
|
||||
@@ -60,4 +50,5 @@ class HomeActivity final : public Activity {
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -66,11 +67,6 @@ void sortFileList(std::vector<std::string>& strs) {
|
||||
});
|
||||
}
|
||||
|
||||
void MyLibraryActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<MyLibraryActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void MyLibraryActivity::loadFiles() {
|
||||
files.clear();
|
||||
|
||||
@@ -109,33 +105,14 @@ void MyLibraryActivity::loadFiles() {
|
||||
void MyLibraryActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
loadFiles();
|
||||
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask",
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void MyLibraryActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
|
||||
files.clear();
|
||||
}
|
||||
|
||||
@@ -146,7 +123,6 @@ void MyLibraryActivity::loop() {
|
||||
basepath = "/";
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,7 +138,7 @@ void MyLibraryActivity::loop() {
|
||||
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
} else {
|
||||
onSelectBook(basepath + files[selectorIndex]);
|
||||
return;
|
||||
@@ -183,7 +159,7 @@ void MyLibraryActivity::loop() {
|
||||
const std::string dirName = oldPath.substr(pos + 1) + "/";
|
||||
selectorIndex = findEntry(dirName);
|
||||
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
} else {
|
||||
onGoHome();
|
||||
}
|
||||
@@ -194,51 +170,39 @@ void MyLibraryActivity::loop() {
|
||||
|
||||
buttonNavigator.onNextRelease([this, listSize] {
|
||||
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousRelease([this, listSize] {
|
||||
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onNextContinuous([this, listSize, pageItems] {
|
||||
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
|
||||
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void MyLibraryActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void MyLibraryActivity::render() const {
|
||||
void MyLibraryActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str();
|
||||
auto folderName = basepath == "/" ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1).c_str();
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName);
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||
if (files.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No books found");
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_BOOKS_FOUND));
|
||||
} else {
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
||||
@@ -246,7 +210,8 @@ void MyLibraryActivity::render() const {
|
||||
}
|
||||
|
||||
// Help text
|
||||
const auto labels = mappedInput.mapLabels(basepath == "/" ? "« Home" : "« Back", "Open", "Up", "Down");
|
||||
const auto labels = mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP),
|
||||
tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -13,12 +9,9 @@
|
||||
|
||||
class MyLibraryActivity final : public Activity {
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
size_t selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
|
||||
// Files state
|
||||
std::string basepath = "/";
|
||||
@@ -28,10 +21,6 @@ class MyLibraryActivity final : public Activity {
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
// Data loading
|
||||
void loadFiles();
|
||||
size_t findEntry(const std::string& name) const;
|
||||
@@ -48,4 +37,5 @@ class MyLibraryActivity final : public Activity {
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -15,11 +16,6 @@ namespace {
|
||||
constexpr unsigned long GO_HOME_MS = 1000;
|
||||
} // namespace
|
||||
|
||||
void RecentBooksActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<RecentBooksActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void RecentBooksActivity::loadRecentBooks() {
|
||||
recentBooks.clear();
|
||||
const auto& books = RECENT_BOOKS.getBooks();
|
||||
@@ -37,34 +33,15 @@ void RecentBooksActivity::loadRecentBooks() {
|
||||
void RecentBooksActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Load data
|
||||
loadRecentBooks();
|
||||
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&RecentBooksActivity::taskTrampoline, "RecentBooksActivityTask",
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void RecentBooksActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
|
||||
recentBooks.clear();
|
||||
}
|
||||
|
||||
@@ -87,52 +64,40 @@ void RecentBooksActivity::loop() {
|
||||
|
||||
buttonNavigator.onNextRelease([this, listSize] {
|
||||
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousRelease([this, listSize] {
|
||||
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onNextContinuous([this, listSize, pageItems] {
|
||||
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
|
||||
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void RecentBooksActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void RecentBooksActivity::render() const {
|
||||
void RecentBooksActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Recent Books");
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_MENU_RECENT_BOOKS));
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||
|
||||
// Recent tab
|
||||
if (recentBooks.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No recent books");
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_RECENT_BOOKS));
|
||||
} else {
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
|
||||
@@ -141,7 +106,7 @@ void RecentBooksActivity::render() const {
|
||||
}
|
||||
|
||||
// Help text
|
||||
const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down");
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -13,12 +11,9 @@
|
||||
|
||||
class RecentBooksActivity final : public Activity {
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
size_t selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
|
||||
// Recent tab state
|
||||
std::vector<RecentBook> recentBooks;
|
||||
@@ -27,10 +22,6 @@ class RecentBooksActivity final : public Activity {
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
// Data loading
|
||||
void loadRecentBooks();
|
||||
|
||||
@@ -42,4 +33,5 @@ class RecentBooksActivity final : public Activity {
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <ESPmDNS.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
@@ -14,16 +15,10 @@ namespace {
|
||||
constexpr const char* HOSTNAME = "crosspoint";
|
||||
} // namespace
|
||||
|
||||
void CalibreConnectActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<CalibreConnectActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
state = CalibreConnectState::WIFI_SELECTION;
|
||||
connectedIP.clear();
|
||||
connectedSSID.clear();
|
||||
@@ -35,13 +30,6 @@ void CalibreConnectActivity::onEnter() {
|
||||
lastCompleteAt = 0;
|
||||
exitRequested = false;
|
||||
|
||||
xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask",
|
||||
2048, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
@@ -63,14 +51,6 @@ void CalibreConnectActivity::onExit() {
|
||||
delay(30);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(30);
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) {
|
||||
@@ -92,7 +72,7 @@ void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) {
|
||||
|
||||
void CalibreConnectActivity::startWebServer() {
|
||||
state = CalibreConnectState::SERVER_STARTING;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
|
||||
if (MDNS.begin(HOSTNAME)) {
|
||||
// mDNS is optional for the Calibre plugin but still helpful for users.
|
||||
@@ -104,10 +84,10 @@ void CalibreConnectActivity::startWebServer() {
|
||||
|
||||
if (webServer->isRunning()) {
|
||||
state = CalibreConnectState::SERVER_RUNNING;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
} else {
|
||||
state = CalibreConnectState::ERROR;
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +158,7 @@ void CalibreConnectActivity::loop() {
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
updateRequired = true;
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,19 +168,7 @@ void CalibreConnectActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::render() const {
|
||||
void CalibreConnectActivity::render(Activity::RenderLock&&) {
|
||||
if (state == CalibreConnectState::SERVER_RUNNING) {
|
||||
renderer.clearScreen();
|
||||
renderServerRunning();
|
||||
@@ -211,9 +179,9 @@ void CalibreConnectActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
if (state == CalibreConnectState::SERVER_STARTING) {
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Calibre...", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CALIBRE_STARTING), true, EpdFontFamily::BOLD);
|
||||
} else if (state == CalibreConnectState::ERROR) {
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Calibre setup failed", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD);
|
||||
}
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -223,31 +191,32 @@ void CalibreConnectActivity::renderServerRunning() const {
|
||||
constexpr int SMALL_SPACING = 20;
|
||||
constexpr int SECTION_SPACING = 40;
|
||||
constexpr int TOP_PADDING = 14;
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Connect to Calibre", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CALIBRE_WIRELESS), true, EpdFontFamily::BOLD);
|
||||
|
||||
int y = 55 + TOP_PADDING;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, "Network", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD);
|
||||
y += LINE_SPACING;
|
||||
std::string ssidInfo = "Network: " + connectedSSID;
|
||||
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str());
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str());
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING,
|
||||
(std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str());
|
||||
|
||||
y += LINE_SPACING * 2 + SECTION_SPACING;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, "Setup", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD);
|
||||
y += LINE_SPACING;
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y, "1) Install CrossPoint Reader plugin");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, "2) Be on the same WiFi network");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, "3) In Calibre: \"Send to device\"");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, "Keep this screen open while sending");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y, tr(STR_CALIBRE_INSTRUCTION_1));
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, tr(STR_CALIBRE_INSTRUCTION_2));
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, tr(STR_CALIBRE_INSTRUCTION_3));
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, tr(STR_CALIBRE_INSTRUCTION_4));
|
||||
|
||||
y += SMALL_SPACING * 3 + SECTION_SPACING;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, "Status", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD);
|
||||
y += LINE_SPACING;
|
||||
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
|
||||
std::string label = "Receiving";
|
||||
std::string label = tr(STR_CALIBRE_RECEIVING);
|
||||
if (!currentUploadName.empty()) {
|
||||
label += ": " + currentUploadName;
|
||||
if (label.length() > 34) {
|
||||
@@ -263,13 +232,13 @@ void CalibreConnectActivity::renderServerRunning() const {
|
||||
}
|
||||
|
||||
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
|
||||
std::string msg = "Received: " + lastCompleteName;
|
||||
std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName;
|
||||
if (msg.length() > 36) {
|
||||
msg.replace(33, msg.length() - 33, "...");
|
||||
}
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str());
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -17,9 +14,6 @@ enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING
|
||||
* but renders Calibre-specific instructions instead of the web transfer UI.
|
||||
*/
|
||||
class CalibreConnectActivity final : public ActivityWithSubactivity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
CalibreConnectState state = CalibreConnectState::WIFI_SELECTION;
|
||||
const std::function<void()> onComplete;
|
||||
|
||||
@@ -34,9 +28,6 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
|
||||
unsigned long lastCompleteAt = 0;
|
||||
bool exitRequested = false;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
void renderServerRunning() const;
|
||||
|
||||
void onWifiSelectionComplete(bool connected);
|
||||
@@ -50,6 +41,7 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
bool skipLoopDelay() override { return webServer && webServer->isRunning(); }
|
||||
bool preventAutoSleep() override { return webServer && webServer->isRunning(); }
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user