Compare commits
112 Commits
0.16.0
...
97c33141bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c33141bd | ||
|
|
2a32d8a182 | ||
|
|
d6f38d4441 | ||
|
|
513d111634 | ||
|
|
ad9137cfdf | ||
|
|
5c80cface7 | ||
|
|
86d3774a8f | ||
|
|
7ba5978848 | ||
|
|
3d47c081f2 | ||
|
|
6702060960 | ||
|
|
0bc6747483 | ||
|
|
00666377de | ||
|
|
22b77edddf | ||
|
|
2e673c753d | ||
|
|
1a30826981 | ||
|
|
50e6ef9bd8 | ||
|
|
a616f42cb4 | ||
|
|
0508bfc1f7 | ||
|
|
6c3a615fac | ||
|
|
46c2109f1f | ||
|
|
5816ab2a47 | ||
|
|
2c0a105550 | ||
|
|
6e51afb977 | ||
|
|
cb24947477 | ||
|
|
7a385d78a4 | ||
|
|
0991782fb4 | ||
|
|
3ae1007cbe | ||
|
|
efb9b72e64 | ||
|
|
4a210823a8 | ||
|
|
f5b85f5ca1 | ||
|
|
7e93411f46 | ||
|
|
44452a42e9 | ||
|
|
0c2df24f5c | ||
|
|
3a12ca2725 | ||
|
|
98e6789626 | ||
|
|
b5d28a3a9c | ||
|
|
14ef625679 | ||
|
|
64d161e88b | ||
|
|
e73bb3213f | ||
|
|
6202bfd651 | ||
|
|
9e04eec072 | ||
|
|
9b04c2ec76 | ||
|
|
def1094411 | ||
|
|
ffddc2472b | ||
|
|
5765bbe821 | ||
|
|
7538e55795 | ||
|
|
21e7d29286 | ||
|
|
b4b028be3a | ||
|
|
f34d7d2aac | ||
|
|
71769490fb | ||
|
|
cda0a3f898 | ||
|
|
7f40c3f477 | ||
|
|
5e52a46837 | ||
|
|
6909f127b4 | ||
|
|
4f0a3aa4dd | ||
|
|
bb983d0ef4 | ||
|
|
b45eaf7ded | ||
|
|
a87eacc6ab | ||
|
|
1caad578fc | ||
|
|
75b0ed7781 | ||
|
|
5b90b68e99 | ||
|
|
67ddd60fce | ||
|
|
76908d38e1 | ||
|
|
e6f5fa43e6 | ||
|
|
e7e31ac487 | ||
|
|
47f3137dee | ||
|
|
d8632eae08 | ||
|
|
9f78fd33e8 | ||
|
|
bd8132a260 | ||
|
|
3223e85ea5 | ||
|
|
f89ce514c8 | ||
|
|
211153fcd5 | ||
|
|
91777a9023 | ||
|
|
d8e813a78d | ||
|
|
c3b9bc38b9 | ||
|
|
fb0af32ec0 | ||
|
|
cb4d86fec6 | ||
|
|
e94f056e8a | ||
|
|
20c5d8ccf8 | ||
|
|
d35bda8023 | ||
|
|
d762325035 | ||
|
|
7f2b1a818e | ||
|
|
ddbe49f536 | ||
|
|
17fedd2a69 | ||
|
|
768c2f8eed | ||
|
|
216dbc8ee3 | ||
|
|
ee987f07ff | ||
|
|
23ecc52261 | ||
|
|
edaf8fff9d | ||
|
|
c8683340ab | ||
|
|
5a9ee19eb8 | ||
|
|
c49a819939 | ||
|
|
bf87a7dc60 | ||
|
|
2cf799f45b | ||
|
|
db659f3ea2 | ||
|
|
78d6e5931c | ||
|
|
dac11c3fdd | ||
|
|
d403044f76 | ||
|
|
f67c544e16 | ||
|
|
e5c0ddc9fa | ||
|
|
b1dcb7733b | ||
|
|
0d82b03981 | ||
|
|
5a97334ace | ||
|
|
4dd73a211a | ||
|
|
634f6279cb | ||
|
|
11b2a59233 | ||
|
|
12c20bb09e | ||
|
|
6b7065b986 | ||
|
|
f4df513bf3 | ||
|
|
f935b59a41 | ||
|
|
da4d3b5ea5 | ||
|
|
172916afd4 |
106
.github/workflows/ci.yml
vendored
106
.github/workflows/ci.yml
vendored
@@ -1,10 +1,55 @@
|
|||||||
name: CI
|
name: CI (build)
|
||||||
'on':
|
|
||||||
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
clang-format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
|
||||||
|
- name: Install clang-format-21
|
||||||
|
run: |
|
||||||
|
wget https://apt.llvm.org/llvm.sh
|
||||||
|
chmod +x llvm.sh
|
||||||
|
sudo ./llvm.sh 21
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y clang-format-21
|
||||||
|
|
||||||
|
- name: Run clang-format
|
||||||
|
run: |
|
||||||
|
PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix
|
||||||
|
git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
|
||||||
|
|
||||||
|
cppcheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
|
||||||
|
- name: Install PlatformIO Core
|
||||||
|
run: pip install --upgrade platformio
|
||||||
|
|
||||||
|
- name: Run cppcheck
|
||||||
|
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -19,19 +64,46 @@ jobs:
|
|||||||
- name: Install PlatformIO Core
|
- name: Install PlatformIO Core
|
||||||
run: pip install --upgrade platformio
|
run: pip install --upgrade platformio
|
||||||
|
|
||||||
- name: Install clang-format-21
|
|
||||||
run: |
|
|
||||||
wget https://apt.llvm.org/llvm.sh
|
|
||||||
chmod +x llvm.sh
|
|
||||||
sudo ./llvm.sh 21
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y clang-format-21
|
|
||||||
|
|
||||||
- name: Run cppcheck
|
|
||||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
|
||||||
|
|
||||||
- name: Run clang-format
|
|
||||||
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
|
|
||||||
|
|
||||||
- name: Build CrossPoint
|
- name: Build CrossPoint
|
||||||
run: pio run
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
pio run | tee pio.log
|
||||||
|
|
||||||
|
- name: Extract firmware stats
|
||||||
|
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
|
||||||
|
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
|
||||||
|
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
|
||||||
|
{
|
||||||
|
echo "## Firmware build stats"
|
||||||
|
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
|
||||||
|
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- name: Upload firmware.bin artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: firmware.bin
|
||||||
|
path: .pio/build/default/firmware.bin
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
|
||||||
|
# PR requirements.
|
||||||
|
test-status:
|
||||||
|
name: Test Status
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
- clang-format
|
||||||
|
- cppcheck
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Fail because needed jobs failed
|
||||||
|
# Fail if any job failed or was cancelled (skipped jobs are ok)
|
||||||
|
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||||
|
run: exit 1
|
||||||
|
- name: Success
|
||||||
|
run: exit 0
|
||||||
|
|||||||
1
.github/workflows/pr-formatting-check.yml
vendored
1
.github/workflows/pr-formatting-check.yml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: "PR Formatting"
|
name: "PR Formatting"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
|||||||
48
.github/workflows/release_candidate.yml
vendored
Normal file
48
.github/workflows/release_candidate.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Compile Release Candidate
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release-candidate:
|
||||||
|
if: startsWith(github.ref_name, 'release/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/pip
|
||||||
|
~/.platformio/.cache
|
||||||
|
key: ${{ runner.os }}-pio
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
|
||||||
|
- name: Install PlatformIO Core
|
||||||
|
run: pip install --upgrade platformio
|
||||||
|
|
||||||
|
- name: Extract env
|
||||||
|
run: |
|
||||||
|
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||||
|
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build CrossPoint Release Candidate
|
||||||
|
env:
|
||||||
|
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
|
||||||
|
run: pio run -e gh_release_rc
|
||||||
|
|
||||||
|
- name: Upload Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
|
||||||
|
path: |
|
||||||
|
.pio/build/gh_release_rc/bootloader.bin
|
||||||
|
.pio/build/gh_release_rc/firmware.bin
|
||||||
|
.pio/build/gh_release_rc/firmware.elf
|
||||||
|
.pio/build/gh_release_rc/firmware.map
|
||||||
|
.pio/build/gh_release_rc/partitions.bin
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
lib/EpdFont/fontsrc
|
lib/EpdFont/fontsrc
|
||||||
|
lib/I18n/I18nStrings.cpp
|
||||||
*.generated.h
|
*.generated.h
|
||||||
.vs
|
.vs
|
||||||
build
|
build
|
||||||
|
|||||||
38
GOVERNANCE.md
Normal file
38
GOVERNANCE.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Project Governance & Community Principles
|
||||||
|
|
||||||
|
CrossPoint Reader is a community-driven, open-source project. Our goal is to provide a high-quality, open-source
|
||||||
|
firmware alternative for the Xteink X4 hardware. To keep this project productive and welcoming as we grow, we ask all
|
||||||
|
contributors to follow these principles.
|
||||||
|
|
||||||
|
### 1. The "Human First" Rule
|
||||||
|
Technical discussions can get heated, but they should never be personal.
|
||||||
|
- **Assume good intent:** We are all volunteers working on this in our free time. If a comment seems abrasive, assume
|
||||||
|
it’s a language barrier or a misunderstanding before taking offense.
|
||||||
|
- **Focus on the code, not the person:** Critique the implementation, the performance, or the UX. Never the intelligence
|
||||||
|
or character of the contributor.
|
||||||
|
- **Inflammatory language:** Personal attacks, trolling, or exclusionary language (based on race, gender, background,
|
||||||
|
etc.) are not welcome here and will be moderated.
|
||||||
|
|
||||||
|
### 2. A "Do-ocracy" with Guidance
|
||||||
|
CrossPoint thrives because people step up to build what they want to see.
|
||||||
|
- If you want a feature, the best way to get it is to start an
|
||||||
|
[Idea Discussion](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) or open a PR.
|
||||||
|
- If you want to report a bug, check for duplicates and create an
|
||||||
|
[Issue](https://github.com/crosspoint-reader/crosspoint-reader/issues).
|
||||||
|
- While we encourage experimentation, the maintainers reserve the right to guide the project’s technical direction to
|
||||||
|
ensure stability on the ESP32-C3’s constrained hardware.
|
||||||
|
- For more guidance on the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
||||||
|
|
||||||
|
### 3. Transparent Communication
|
||||||
|
To keep the project healthy, we keep our "work" in the open.
|
||||||
|
- **Public by Default:** All technical decisions and project management discussions happen in GitHub Issues, Pull
|
||||||
|
Requests, or the public Discussions tab.
|
||||||
|
- **Clarity in Writing:** Because we have a global community with different levels of English proficiency, please be as
|
||||||
|
explicit and clear as possible in your PR descriptions and bug reports.
|
||||||
|
|
||||||
|
### 4. Moderation & Safety
|
||||||
|
The maintainers are responsible for keeping the community a safe place to contribute.
|
||||||
|
- We reserve the right to hide comments, lock threads, or block users who repeatedly violate these principles or engage
|
||||||
|
in harassment.
|
||||||
|
- **Reporting:** If you feel you are being harassed or see behavior that is damaging the community, please reach out
|
||||||
|
privately to @daveallie.
|
||||||
32
README.md
32
README.md
@@ -45,11 +45,13 @@ Multi-language support: Read EPUBs in various languages, including English, Span
|
|||||||
|
|
||||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||||
|
|
||||||
|
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
### Web (latest firmware)
|
### Web (latest firmware)
|
||||||
|
|
||||||
1. Connect your Xteink X4 to your computer via USB-C
|
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
||||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||||
|
|
||||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||||
@@ -58,7 +60,7 @@ back to the other partition using the "Swap boot partition" button here https://
|
|||||||
### Web (specific firmware version)
|
### Web (specific firmware version)
|
||||||
|
|
||||||
1. Connect your Xteink X4 to your computer via USB-C
|
1. Connect your Xteink X4 to your computer via USB-C
|
||||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
|
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
|
||||||
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
||||||
|
|
||||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||||
@@ -82,7 +84,7 @@ See [Development](#development) below.
|
|||||||
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
|
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone --recursive https://github.com/daveallie/crosspoint-reader
|
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
||||||
|
|
||||||
# Or, if you've already cloned without --recursive:
|
# Or, if you've already cloned without --recursive:
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
@@ -95,6 +97,25 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
|
|||||||
```sh
|
```sh
|
||||||
pio run --target upload
|
pio run --target upload
|
||||||
```
|
```
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
||||||
|
|
||||||
|
First, make sure all required Python packages are installed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m pip install pyserial colorama matplotlib
|
||||||
|
```
|
||||||
|
after that run the script:
|
||||||
|
```sh
|
||||||
|
# For Linux
|
||||||
|
# This was tested on Debian and should work on most Linux systems.
|
||||||
|
python3 scripts/debugging_monitor.py
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
||||||
|
```
|
||||||
|
Minor adjustments may be required for Windows.
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
|
|
||||||
@@ -133,9 +154,12 @@ For more details on the internal file structures, see the [file formats document
|
|||||||
|
|
||||||
Contributions are very welcome!
|
Contributions are very welcome!
|
||||||
|
|
||||||
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/crosspoint-reader/discussions/categories/ideas).
|
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
|
||||||
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
|
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
|
||||||
|
|
||||||
|
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
|
||||||
|
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
|
||||||
|
|
||||||
### To submit a contribution:
|
### To submit a contribution:
|
||||||
|
|
||||||
1. Fork the repo
|
1. Fork the repo
|
||||||
|
|||||||
50
SCOPE.md
Normal file
50
SCOPE.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Project Vision & Scope: CrossPoint Reader
|
||||||
|
|
||||||
|
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
|
||||||
|
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
||||||
|
|
||||||
|
## 1. Core Mission
|
||||||
|
|
||||||
|
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
|
||||||
|
usability over "swiss-army-knife" functionality.
|
||||||
|
|
||||||
|
## 2. Scope
|
||||||
|
|
||||||
|
### In-Scope
|
||||||
|
|
||||||
|
*These are features that directly improve the primary purpose of the device.*
|
||||||
|
|
||||||
|
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
|
||||||
|
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
|
||||||
|
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
|
||||||
|
engine.
|
||||||
|
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
|
||||||
|
capabilities.
|
||||||
|
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
|
||||||
|
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||||
|
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
||||||
|
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
||||||
|
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
||||||
|
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
||||||
|
without breaking focus.
|
||||||
|
|
||||||
|
### Out-of-Scope
|
||||||
|
|
||||||
|
*These items are rejected because they compromise the device's stability or mission.*
|
||||||
|
|
||||||
|
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
|
||||||
|
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
||||||
|
and complicate the single-core CPU's execution.
|
||||||
|
* **Media Playback:** No Audio players or Audio-books.
|
||||||
|
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||||
|
capabilities and more powerful chips.
|
||||||
|
|
||||||
|
## 3. Idea Evaluation
|
||||||
|
|
||||||
|
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
|
||||||
|
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
|
||||||
|
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
|
||||||
|
and, critically, not distract from that reading experience.
|
||||||
|
|
||||||
|
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
|
||||||
|
> coding!
|
||||||
@@ -13,14 +13,17 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [3.2 Book Selection](#32-book-selection)
|
- [3.2 Book Selection](#32-book-selection)
|
||||||
- [3.3 Reading Mode](#33-reading-mode)
|
- [3.3 Reading Mode](#33-reading-mode)
|
||||||
- [3.4 File Upload Screen](#34-file-upload-screen)
|
- [3.4 File Upload Screen](#34-file-upload-screen)
|
||||||
|
- [3.4.1 Calibre Wireless Transfers](#341-calibre-wireless-transfers)
|
||||||
- [3.5 Settings](#35-settings)
|
- [3.5 Settings](#35-settings)
|
||||||
- [3.6 Sleep Screen](#36-sleep-screen)
|
- [3.6 Sleep Screen](#36-sleep-screen)
|
||||||
- [4. Reading Mode](#4-reading-mode)
|
- [4. Reading Mode](#4-reading-mode)
|
||||||
- [Page Turning](#page-turning)
|
- [Page Turning](#page-turning)
|
||||||
- [Chapter Navigation](#chapter-navigation)
|
- [Chapter Navigation](#chapter-navigation)
|
||||||
- [System Navigation](#system-navigation)
|
- [System Navigation](#system-navigation)
|
||||||
|
- [Supported Languages](#supported-languages)
|
||||||
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
|
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
|
||||||
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||||
|
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||||
|
|
||||||
|
|
||||||
## 1. Hardware Overview
|
## 1. Hardware Overview
|
||||||
@@ -102,6 +105,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
|
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
|
||||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||||
- "None" - A blank screen
|
- "None" - A blank screen
|
||||||
|
- "Cover + Custom" - The book cover image, fallbacks to "Custom" behavior
|
||||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||||
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
||||||
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
|
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
|
||||||
@@ -112,7 +116,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- **Status Bar**: Configure the status bar displayed while reading:
|
- **Status Bar**: Configure the status bar displayed while reading:
|
||||||
- "None" - No status bar
|
- "None" - No status bar
|
||||||
- "No Progress" - Show status bar without reading progress
|
- "No Progress" - Show status bar without reading progress
|
||||||
- "Full" - Show status bar with reading progress
|
- "Full w/ Percentage" - Show status bar with book progress (as percentage)
|
||||||
|
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
|
||||||
|
- "Book Bar Only" - Show book progress (as bar)
|
||||||
|
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
|
||||||
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
|
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
|
||||||
- "Never" - Always show battery percentage (default)
|
- "Never" - Always show battery percentage (default)
|
||||||
- "In Reader" - Show battery percentage everywhere except in reading mode
|
- "In Reader" - Show battery percentage everywhere except in reading mode
|
||||||
@@ -148,6 +155,9 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
|
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
|
||||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
|
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
|
||||||
|
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight
|
||||||
|
- "OFF" (default) - Disable the fix
|
||||||
|
- "ON" - Enable the fix
|
||||||
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
|
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
|
||||||
- **Check for updates**: Check for firmware updates over WiFi.
|
- **Check for updates**: Check for firmware updates over WiFi.
|
||||||
|
|
||||||
@@ -201,7 +211,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s
|
|||||||
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
|
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
|
||||||
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
||||||
|
|
||||||
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
|
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -220,3 +230,18 @@ Accessible by pressing **Confirm** while inside a book.
|
|||||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||||
|
|
||||||
* **Images:** Embedded images in e-books will not render.
|
* **Images:** Embedded images in e-books will not render.
|
||||||
|
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Troubleshooting Issues & Escaping Bootloop
|
||||||
|
|
||||||
|
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
|
||||||
|
|
||||||
|
```
|
||||||
|
pio device monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
||||||
|
|
||||||
|
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Check if clang-format is availible
|
||||||
|
command -v clang-format >/dev/null 2>&1 || {
|
||||||
|
printf "'clang-format' not found in current environment\n"
|
||||||
|
printf "install 'clang', 'clang-tools', or 'clang-format' depending on your distro/os and tooling requirements\n"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
GIT_LS_FILES_FLAGS=""
|
GIT_LS_FILES_FLAGS=""
|
||||||
if [[ "$1" == "-g" ]]; then
|
if [[ "$1" == "-g" ]]; then
|
||||||
GIT_LS_FILES_FLAGS="--modified"
|
GIT_LS_FILES_FLAGS="--modified"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# --- Main Logic ---
|
# --- Main Logic ---
|
||||||
|
|
||||||
# Format all files (or only modified files if -g is passed)
|
# Format all files (or only modified files if -g is passed)
|
||||||
@@ -13,7 +21,9 @@ fi
|
|||||||
# --modified: files tracked by git that have been modified (staged or unstaged)
|
# --modified: files tracked by git that have been modified (staged or unstaged)
|
||||||
# --exclude-standard: ignores files in .gitignore
|
# --exclude-standard: ignores files in .gitignore
|
||||||
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
||||||
|
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
|
||||||
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||||
|
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||||
| xargs -r clang-format -style=file -i
|
| xargs -r clang-format -style=file -i
|
||||||
|
|||||||
@@ -45,22 +45,9 @@ byte arrays, and emits headers under
|
|||||||
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
|
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
|
||||||
in flash.
|
in flash.
|
||||||
|
|
||||||
To refresh the firmware assets after updating the `.bin` files, run:
|
A convenient script `update_hyphenation.sh` is used to update all languages.
|
||||||
|
To use it, run:
|
||||||
|
|
||||||
```
|
```sh
|
||||||
./scripts/generate_hyphenation_trie.py \
|
./scripts/update_hypenation.sh
|
||||||
--input lib/Epub/Epub/hyphenation/tries/en.bin \
|
|
||||||
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
|
|
||||||
|
|
||||||
./scripts/generate_hyphenation_trie.py \
|
|
||||||
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
|
|
||||||
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
|
|
||||||
|
|
||||||
./scripts/generate_hyphenation_trie.py \
|
|
||||||
--input lib/Epub/Epub/hyphenation/tries/de.bin \
|
|
||||||
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
|
|
||||||
|
|
||||||
./scripts/generate_hyphenation_trie.py \
|
|
||||||
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
|
|
||||||
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
|
|
||||||
```
|
```
|
||||||
|
|||||||
237
docs/i18n.md
Normal file
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
|
# Web Server Guide
|
||||||
|
|
||||||
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone.
|
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload files from your computer or phone.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
CrossPoint Reader includes a built-in web server that allows you to:
|
CrossPoint Reader includes a built-in web server that allows you to:
|
||||||
|
|
||||||
- Upload EPUB files wirelessly from any device on the same WiFi network
|
- Upload files wirelessly from any device on the same WiFi network
|
||||||
- Browse and manage files on your device's SD card
|
- Browse and manage files on your device's SD card
|
||||||
- Create folders to organize your ebooks
|
- Create folders to organize your library
|
||||||
- Delete files and folders
|
- Delete files and folders
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
@@ -129,34 +129,31 @@ Click **File Manager** to access file management features.
|
|||||||
#### Browsing Files
|
#### Browsing Files
|
||||||
|
|
||||||
- The file manager displays all files and folders on your SD card
|
- The file manager displays all files and folders on your SD card
|
||||||
- **Folders** are highlighted in yellow with a 📁 icon
|
- **Folders** are highlighted in yellow and indicated with a 📁 icon
|
||||||
- **EPUB files** are highlighted in green with a 📗 icon
|
- **EPUB Files** are highlighted in green and indicated with a 📗 icon
|
||||||
|
- **All Other Files** are not highlighted and indicated with a 📄 icon
|
||||||
- Click on a folder name to navigate into it
|
- Click on a folder name to navigate into it
|
||||||
- Use the breadcrumb navigation at the top to go back to parent folders
|
- Use the breadcrumb navigation at the top to go back to parent folders
|
||||||
|
|
||||||
<img src="./images/wifi/webserver_files.png" width="600">
|
<img src="./images/wifi/webserver_files.png" width="600">
|
||||||
|
|
||||||
#### Uploading EPUB Files
|
#### Uploading Files
|
||||||
|
|
||||||
1. Click the **+ Add** button in the top-right corner
|
1. Click the **📤 Upload** button in the top-right corner
|
||||||
2. Select **Upload eBook** from the dropdown menu
|
2. Click **Choose File** and select a file from your device
|
||||||
3. Click **Choose File** and select an `.epub` file from your device
|
3. Click **Upload**
|
||||||
4. Click **Upload**
|
4. A progress bar will show the upload status
|
||||||
5. A progress bar will show the upload status
|
5. The page will automatically refresh when the upload is complete
|
||||||
6. The page will automatically refresh when the upload is complete
|
|
||||||
|
|
||||||
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
|
|
||||||
|
|
||||||
<img src="./images/wifi/webserver_upload.png" width="600">
|
<img src="./images/wifi/webserver_upload.png" width="600">
|
||||||
|
|
||||||
#### Creating Folders
|
#### Creating Folders
|
||||||
|
|
||||||
1. Click the **+ Add** button in the top-right corner
|
1. Click the **📁 New Folder** button in the top-right corner
|
||||||
2. Select **New Folder** from the dropdown menu
|
2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||||
3. Enter a folder name (letters, numbers, underscores, and hyphens only)
|
3. Click **Create Folder**
|
||||||
4. Click **Create Folder**
|
|
||||||
|
|
||||||
This is useful for organizing your ebooks by genre, author, or series.
|
This is useful for organizing your library by genre, author, series or file type.
|
||||||
|
|
||||||
#### Deleting Files and Folders
|
#### Deleting Files and Folders
|
||||||
|
|
||||||
@@ -168,11 +165,25 @@ This is useful for organizing your ebooks by genre, author, or series.
|
|||||||
|
|
||||||
**Note:** Folders must be empty before they can be deleted.
|
**Note:** Folders must be empty before they can be deleted.
|
||||||
|
|
||||||
|
#### Moving Files
|
||||||
|
|
||||||
|
1. Click the **📂** (folder) icon next to any file
|
||||||
|
2. Enter a folder name or select one from the dropdown
|
||||||
|
3. Click **Move** to relocate the file
|
||||||
|
|
||||||
|
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
|
||||||
|
|
||||||
|
#### Renaming Files
|
||||||
|
|
||||||
|
1. Click the **✏️** (pencil) icon next to any file
|
||||||
|
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||||
|
3. Click **Rename** to permanently rename the file
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Command Line File Management
|
## Command Line File Management
|
||||||
|
|
||||||
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md).
|
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. Detailed documentation can be found [here](./webserver-endpoints.md).
|
||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
@@ -189,7 +200,6 @@ For power users, you can manage files directly from your terminal using `curl` w
|
|||||||
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
|
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
|
||||||
- **Web Server Port:** 80 (HTTP)
|
- **Web Server Port:** 80 (HTTP)
|
||||||
- **Maximum Upload Size:** Limited by available SD card space
|
- **Maximum Upload Size:** Limited by available SD card space
|
||||||
- **Supported File Format:** `.epub` only
|
|
||||||
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
|
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -198,7 +208,7 @@ For power users, you can manage files directly from your terminal using `curl` w
|
|||||||
|
|
||||||
1. **Organize with folders** - Create folders before uploading to keep your library organized
|
1. **Organize with folders** - Create folders before uploading to keep your library organized
|
||||||
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
|
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
|
||||||
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload
|
3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
|
||||||
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
|
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
|
||||||
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
|
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
|
||||||
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
|
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
#include "EpdFontFamily.h"
|
#include "EpdFontFamily.h"
|
||||||
|
|
||||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||||
if (style == BOLD && bold) {
|
// Extract font style bits (ignore UNDERLINE bit for font selection)
|
||||||
|
const bool hasBold = (style & BOLD) != 0;
|
||||||
|
const bool hasItalic = (style & ITALIC) != 0;
|
||||||
|
|
||||||
|
if (hasBold && hasItalic) {
|
||||||
|
if (boldItalic) return boldItalic;
|
||||||
|
if (bold) return bold;
|
||||||
|
if (italic) return italic;
|
||||||
|
} else if (hasBold && bold) {
|
||||||
return bold;
|
return bold;
|
||||||
}
|
} else if (hasItalic && italic) {
|
||||||
if (style == ITALIC && italic) {
|
|
||||||
return italic;
|
return italic;
|
||||||
}
|
}
|
||||||
if (style == BOLD_ITALIC) {
|
|
||||||
if (boldItalic) {
|
|
||||||
return boldItalic;
|
|
||||||
}
|
|
||||||
if (bold) {
|
|
||||||
return bold;
|
|
||||||
}
|
|
||||||
if (italic) {
|
|
||||||
return italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return regular;
|
return regular;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
class EpdFontFamily {
|
class EpdFontFamily {
|
||||||
public:
|
public:
|
||||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
|
||||||
|
|
||||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||||
const EpdFont* boldItalic = nullptr)
|
const EpdFont* boldItalic = nullptr)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
|
#include <PngToBmpConverter.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
@@ -17,7 +18,7 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
|
|
||||||
// Get file size without loading it all into heap
|
// Get file size without loading it all into heap
|
||||||
if (!getItemSize(containerPath, &containerSize)) {
|
if (!getItemSize(containerPath, &containerSize)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
|
LOG_ERR("EBP", "Could not find or size META-INF/container.xml");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,13 +30,13 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
|
|
||||||
// Stream read (reusing your existing stream logic)
|
// Stream read (reusing your existing stream logic)
|
||||||
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
LOG_ERR("EBP", "Could not read META-INF/container.xml");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the result
|
// Extract the result
|
||||||
if (containerParser.fullPath.empty()) {
|
if (containerParser.fullPath.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
LOG_ERR("EBP", "Could not find valid rootfile in container.xml");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,28 +47,28 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||||
std::string contentOpfFilePath;
|
std::string contentOpfFilePath;
|
||||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
LOG_ERR("EBP", "Could not find content.opf in zip");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str());
|
||||||
|
|
||||||
size_t contentOpfSize;
|
size_t contentOpfSize;
|
||||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
LOG_ERR("EBP", "Could not get size of content.opf");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||||
if (!opfParser.setup()) {
|
if (!opfParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
LOG_ERR("EBP", "Could not setup content.opf parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
LOG_ERR("EBP", "Could not read content.opf");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +77,54 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
bookMetadata.author = opfParser.author;
|
bookMetadata.author = opfParser.author;
|
||||||
bookMetadata.language = opfParser.language;
|
bookMetadata.language = opfParser.language;
|
||||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
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;
|
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||||
|
|
||||||
if (!opfParser.tocNcxPath.empty()) {
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
@@ -86,27 +135,31 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
tocNavItem = opfParser.tocNavPath;
|
tocNavItem = opfParser.tocNavPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
if (!opfParser.cssFiles.empty()) {
|
||||||
|
cssFiles = opfParser.cssFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Successfully parsed content.opf");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseTocNcxFile() const {
|
bool Epub::parseTocNcxFile() const {
|
||||||
// the ncx file should have been specified in the content.opf file
|
// the ncx file should have been specified in the content.opf file
|
||||||
if (tocNcxItem.empty()) {
|
if (tocNcxItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
LOG_DBG("EBP", "No ncx file specified");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||||
|
|
||||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||||
FsFile tempNcxFile;
|
FsFile tempNcxFile;
|
||||||
if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto ncxSize = tempNcxFile.size();
|
const auto ncxSize = tempNcxFile.size();
|
||||||
@@ -114,14 +167,14 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!ncxParser.setup()) {
|
if (!ncxParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
LOG_ERR("EBP", "Could not setup toc ncx parser");
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
if (!ncxBuffer) {
|
if (!ncxBuffer) {
|
||||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -132,7 +185,7 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
if (processedSize != readSize) {
|
||||||
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
|
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||||
free(ncxBuffer);
|
free(ncxBuffer);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -141,29 +194,29 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
|
|
||||||
free(ncxBuffer);
|
free(ncxBuffer);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
SdMan.remove(tmpNcxPath.c_str());
|
Storage.remove(tmpNcxPath.c_str());
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
LOG_DBG("EBP", "Parsed TOC items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseTocNavFile() const {
|
bool Epub::parseTocNavFile() const {
|
||||||
// the nav file should have been specified in the content.opf file (EPUB 3)
|
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||||
if (tocNavItem.empty()) {
|
if (tocNavItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No nav file specified\n", millis());
|
LOG_DBG("EBP", "No nav file specified");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing toc nav file: %s\n", millis(), tocNavItem.c_str());
|
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||||
|
|
||||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||||
FsFile tempNavFile;
|
FsFile tempNavFile;
|
||||||
if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||||
tempNavFile.close();
|
tempNavFile.close();
|
||||||
if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto navSize = tempNavFile.size();
|
const auto navSize = tempNavFile.size();
|
||||||
@@ -174,13 +227,13 @@ bool Epub::parseTocNavFile() const {
|
|||||||
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!navParser.setup()) {
|
if (!navParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc nav parser\n", millis());
|
LOG_ERR("EBP", "Could not setup toc nav parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
if (!navBuffer) {
|
if (!navBuffer) {
|
||||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc nav parser\n", millis());
|
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +242,7 @@ bool Epub::parseTocNavFile() const {
|
|||||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
if (processedSize != readSize) {
|
||||||
Serial.printf("[%lu] [EBP] Could not process all toc nav data\n", millis());
|
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||||
free(navBuffer);
|
free(navBuffer);
|
||||||
tempNavFile.close();
|
tempNavFile.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -198,22 +251,80 @@ bool Epub::parseTocNavFile() const {
|
|||||||
|
|
||||||
free(navBuffer);
|
free(navBuffer);
|
||||||
tempNavFile.close();
|
tempNavFile.close();
|
||||||
SdMan.remove(tmpNavPath.c_str());
|
Storage.remove(tmpNavPath.c_str());
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis());
|
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Epub::parseCssFiles() const {
|
||||||
|
if (cssFiles.empty()) {
|
||||||
|
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if we have a cached version of the CSS rules
|
||||||
|
if (!cssParser->hasCache()) {
|
||||||
|
// No cache yet - parse CSS files
|
||||||
|
for (const auto& cssPath : cssFiles) {
|
||||||
|
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||||
|
|
||||||
|
// Extract CSS file to temp location
|
||||||
|
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||||
|
FsFile tempCssFile;
|
||||||
|
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||||
|
LOG_ERR("EBP", "Could not create temp CSS file");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str());
|
||||||
|
tempCssFile.close();
|
||||||
|
Storage.remove(tmpCssPath.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tempCssFile.close();
|
||||||
|
|
||||||
|
// Parse the CSS file
|
||||||
|
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||||
|
LOG_ERR("EBP", "Could not open temp CSS file for reading");
|
||||||
|
Storage.remove(tmpCssPath.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cssParser->loadFromStream(tempCssFile);
|
||||||
|
tempCssFile.close();
|
||||||
|
Storage.remove(tmpCssPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to cache for next time
|
||||||
|
if (!cssParser->saveToCache()) {
|
||||||
|
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
||||||
|
}
|
||||||
|
cssParser->clear();
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load in the meta data for the epub file
|
// load in the meta data for the epub file
|
||||||
bool Epub::load(const bool buildIfMissing) {
|
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str());
|
||||||
|
|
||||||
// Initialize spine/TOC cache
|
// Initialize spine/TOC cache
|
||||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||||
|
cssParser.reset(new CssParser(cachePath));
|
||||||
|
|
||||||
// Try to load existing cache first
|
// Try to load existing cache first
|
||||||
if (bookMetadataCache->load()) {
|
if (bookMetadataCache->load()) {
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
if (!skipLoadingCss && !cssParser->hasCache()) {
|
||||||
|
LOG_DBG("EBP", "Warning: CSS rules cache not found, attempting to parse CSS files");
|
||||||
|
// to get CSS file list
|
||||||
|
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||||
|
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
||||||
|
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||||
|
}
|
||||||
|
parseCssFiles();
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,14 +334,14 @@ bool Epub::load(const bool buildIfMissing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache doesn't exist or is invalid, build it
|
// Cache doesn't exist or is invalid, build it
|
||||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
LOG_DBG("EBP", "Cache not found, building spine/TOC cache");
|
||||||
setupCacheDir();
|
setupCacheDir();
|
||||||
|
|
||||||
const uint32_t indexingStart = millis();
|
const uint32_t indexingStart = millis();
|
||||||
|
|
||||||
// Begin building cache - stream entries to disk immediately
|
// Begin building cache - stream entries to disk immediately
|
||||||
if (!bookMetadataCache->beginWrite()) {
|
if (!bookMetadataCache->beginWrite()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
LOG_ERR("EBP", "Could not begin writing cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,23 +349,23 @@ bool Epub::load(const bool buildIfMissing) {
|
|||||||
const uint32_t opfStart = millis();
|
const uint32_t opfStart = millis();
|
||||||
BookMetadataCache::BookMetadata bookMetadata;
|
BookMetadataCache::BookMetadata bookMetadata;
|
||||||
if (!bookMetadataCache->beginContentOpfPass()) {
|
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
LOG_ERR("EBP", "Could not begin writing content.opf pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!parseContentOpf(bookMetadata)) {
|
if (!parseContentOpf(bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
LOG_ERR("EBP", "Could not parse content.opf");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!bookMetadataCache->endContentOpfPass()) {
|
if (!bookMetadataCache->endContentOpfPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
LOG_ERR("EBP", "Could not end writing content.opf pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
|
LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart);
|
||||||
|
|
||||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||||
const uint32_t tocStart = millis();
|
const uint32_t tocStart = millis();
|
||||||
if (!bookMetadataCache->beginTocPass()) {
|
if (!bookMetadataCache->beginTocPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
LOG_ERR("EBP", "Could not begin writing toc pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,78 +373,83 @@ bool Epub::load(const bool buildIfMissing) {
|
|||||||
|
|
||||||
// Try EPUB 3 nav document first (preferred)
|
// Try EPUB 3 nav document first (preferred)
|
||||||
if (!tocNavItem.empty()) {
|
if (!tocNavItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Attempting to parse EPUB 3 nav document\n", millis());
|
LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document");
|
||||||
tocParsed = parseTocNavFile();
|
tocParsed = parseTocNavFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to NCX if nav parsing failed or wasn't available
|
// Fall back to NCX if nav parsing failed or wasn't available
|
||||||
if (!tocParsed && !tocNcxItem.empty()) {
|
if (!tocParsed && !tocNcxItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Falling back to NCX TOC\n", millis());
|
LOG_DBG("EBP", "Falling back to NCX TOC");
|
||||||
tocParsed = parseTocNcxFile();
|
tocParsed = parseTocNcxFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tocParsed) {
|
if (!tocParsed) {
|
||||||
Serial.printf("[%lu] [EBP] Warning: Could not parse any TOC format\n", millis());
|
LOG_ERR("EBP", "Warning: Could not parse any TOC format");
|
||||||
// Continue anyway - book will work without TOC
|
// Continue anyway - book will work without TOC
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache->endTocPass()) {
|
if (!bookMetadataCache->endTocPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
LOG_ERR("EBP", "Could not end writing toc pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
|
LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart);
|
||||||
|
|
||||||
// Close the cache files
|
// Close the cache files
|
||||||
if (!bookMetadataCache->endWrite()) {
|
if (!bookMetadataCache->endWrite()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
LOG_ERR("EBP", "Could not end writing cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build final book.bin
|
// Build final book.bin
|
||||||
const uint32_t buildStart = millis();
|
const uint32_t buildStart = millis();
|
||||||
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
LOG_ERR("EBP", "Could not update mappings and sizes");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
|
LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart);
|
||||||
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
|
LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart);
|
||||||
|
|
||||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
LOG_DBG("EBP", "Could not cleanup tmp files - ignoring");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the cache from disk so it's in the correct state
|
// Reload the cache from disk so it's in the correct state
|
||||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
if (!bookMetadataCache->load()) {
|
if (!bookMetadataCache->load()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
LOG_ERR("EBP", "Failed to reload cache after writing");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
if (!skipLoadingCss) {
|
||||||
|
// Parse CSS files after cache reload
|
||||||
|
parseCssFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::clearCache() const {
|
bool Epub::clearCache() const {
|
||||||
if (!SdMan.exists(cachePath.c_str())) {
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
LOG_DBG("EPB", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
if (!Storage.removeDir(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
LOG_ERR("EPB", "Failed to clear cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
|
LOG_DBG("EPB", "Cache cleared successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Epub::setupCacheDir() const {
|
void Epub::setupCacheDir() const {
|
||||||
if (SdMan.exists(cachePath.c_str())) {
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SdMan.mkdir(cachePath.c_str());
|
Storage.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||||
@@ -374,127 +490,194 @@ std::string Epub::getCoverBmpPath(bool cropped) const {
|
|||||||
|
|
||||||
bool Epub::generateCoverBmp(bool cropped) const {
|
bool Epub::generateCoverBmp(bool cropped) const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getCoverBmpPath(cropped).c_str())) {
|
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
if (coverImageHref.empty()) {
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
LOG_ERR("EBP", "No known cover image");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
|
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
FsFile coverJpg;
|
FsFile coverJpg;
|
||||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile coverBmp;
|
FsFile coverBmp;
|
||||||
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
SdMan.remove(coverJpgTempPath.c_str());
|
Storage.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||||
SdMan.remove(getCoverBmpPath(cropped).c_str());
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
|
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
return success;
|
return success;
|
||||||
} else {
|
|
||||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||||
|
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||||
|
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||||
|
|
||||||
|
FsFile coverPng;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||||
|
coverPng.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile coverBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
|
coverPng.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
|
||||||
|
coverPng.close();
|
||||||
|
coverBmp.close();
|
||||||
|
Storage.remove(coverPngTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
|
||||||
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||||
|
std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||||
|
|
||||||
bool Epub::generateThumbBmp() const {
|
bool Epub::generateThumbBmp(int height) const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
|
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
if (coverImageHref.empty()) {
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||||
return false;
|
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
}
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
|
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
|
||||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
|
||||||
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
|
||||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
FsFile coverJpg;
|
FsFile coverJpg;
|
||||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile thumbBmp;
|
FsFile thumbBmp;
|
||||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Use smaller target size for Continue Reading card (half of screen: 240x400)
|
// Use smaller target size for Continue Reading card (half of screen: 240x400)
|
||||||
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
int THUMB_TARGET_HEIGHT = height;
|
||||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||||
THUMB_TARGET_HEIGHT);
|
THUMB_TARGET_HEIGHT);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
SdMan.remove(coverJpgTempPath.c_str());
|
Storage.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image");
|
||||||
SdMan.remove(getThumbBmpPath().c_str());
|
Storage.remove(getThumbBmpPath(height).c_str());
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
success ? "yes" : "no");
|
return success;
|
||||||
|
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||||
|
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||||
|
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||||
|
|
||||||
|
FsFile coverPng;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||||
|
coverPng.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile thumbBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||||
|
coverPng.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||||
|
int THUMB_TARGET_HEIGHT = height;
|
||||||
|
const bool success =
|
||||||
|
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
||||||
|
coverPng.close();
|
||||||
|
thumbBmp.close();
|
||||||
|
Storage.remove(coverPngTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
|
||||||
|
Storage.remove(getThumbBmpPath(height).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||||
return success;
|
return success;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write an empty bmp file to avoid generation attempts in the future
|
||||||
|
FsFile thumbBmp;
|
||||||
|
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||||
|
thumbBmp.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||||
if (itemHref.empty()) {
|
if (itemHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +685,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
|||||||
|
|
||||||
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
LOG_DBG("EBP", "Failed to read item %s", path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +694,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
|||||||
|
|
||||||
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
||||||
if (itemHref.empty()) {
|
if (itemHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,12 +718,12 @@ size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return get
|
|||||||
|
|
||||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
LOG_ERR("EBP", "getSpineItem called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
LOG_ERR("EBP", "getSpineItem index:%d is out of range", spineIndex);
|
||||||
return bookMetadataCache->getSpineEntry(0);
|
return bookMetadataCache->getSpineEntry(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,12 +732,12 @@ BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
|||||||
|
|
||||||
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
LOG_DBG("EBP", "getTocItem called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,18 +755,18 @@ int Epub::getTocItemsCount() const {
|
|||||||
// work out the section index for a toc index
|
// work out the section index for a toc index
|
||||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
LOG_ERR("EBP", "getSpineIndexForTocIndex called but cache not loaded");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||||
if (spineIndex < 0) {
|
if (spineIndex < 0) {
|
||||||
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
LOG_DBG("EBP", "Section not found for TOC index %d", tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,14 +784,13 @@ size_t Epub::getBookSize() const {
|
|||||||
|
|
||||||
int Epub::getSpineIndexForTextReference() const {
|
int Epub::getSpineIndexForTextReference() const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
|
LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(),
|
LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s",
|
||||||
bookMetadataCache->coreMetadata.coverItemHref.size(),
|
bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||||
bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||||
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
|
||||||
|
|
||||||
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||||
// there was no textReference in epub, so we return 0 (the first chapter)
|
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||||
@@ -618,13 +800,13 @@ int Epub::getSpineIndexForTextReference() const {
|
|||||||
// loop through spine items to get the correct index matching the text href
|
// loop through spine items to get the correct index matching the text href
|
||||||
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||||
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||||
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
|
LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(),
|
||||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
|
i);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This should not happen, as we checked for empty textReferenceHref earlier
|
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||||
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
|
LOG_DBG("EBP", "Section not found for text reference");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Epub/BookMetadataCache.h"
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
#include "Epub/css/CssParser.h"
|
||||||
|
|
||||||
class ZipFile;
|
class ZipFile;
|
||||||
|
|
||||||
@@ -24,11 +25,16 @@ class Epub {
|
|||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
// Spine and TOC cache
|
// Spine and TOC cache
|
||||||
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||||
|
// CSS parser for styling
|
||||||
|
std::unique_ptr<CssParser> cssParser;
|
||||||
|
// CSS files
|
||||||
|
std::vector<std::string> cssFiles;
|
||||||
|
|
||||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||||
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||||
bool parseTocNcxFile() const;
|
bool parseTocNcxFile() const;
|
||||||
bool parseTocNavFile() const;
|
bool parseTocNavFile() const;
|
||||||
|
void parseCssFiles() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||||
@@ -37,7 +43,7 @@ class Epub {
|
|||||||
}
|
}
|
||||||
~Epub() = default;
|
~Epub() = default;
|
||||||
std::string& getBasePath() { return contentBasePath; }
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
bool load(bool buildIfMissing = true);
|
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
const std::string& getCachePath() const;
|
const std::string& getCachePath() const;
|
||||||
@@ -48,7 +54,8 @@ class Epub {
|
|||||||
std::string getCoverBmpPath(bool cropped = false) const;
|
std::string getCoverBmpPath(bool cropped = false) const;
|
||||||
bool generateCoverBmp(bool cropped = false) const;
|
bool generateCoverBmp(bool cropped = false) const;
|
||||||
std::string getThumbBmpPath() const;
|
std::string getThumbBmpPath() const;
|
||||||
bool generateThumbBmp() const;
|
std::string getThumbBmpPath(int height) const;
|
||||||
|
bool generateThumbBmp(int height) const;
|
||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
@@ -64,4 +71,5 @@ class Epub {
|
|||||||
|
|
||||||
size_t getBookSize() const;
|
size_t getBookSize() const;
|
||||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||||
|
CssParser* getCssParser() const { return cssParser.get(); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "BookMetadataCache.h"
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
@@ -21,15 +21,15 @@ bool BookMetadataCache::beginWrite() {
|
|||||||
buildMode = true;
|
buildMode = true;
|
||||||
spineCount = 0;
|
spineCount = 0;
|
||||||
tocCount = 0;
|
tocCount = 0;
|
||||||
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
LOG_DBG("BMC", "Entering write mode");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::beginContentOpfPass() {
|
bool BookMetadataCache::beginContentOpfPass() {
|
||||||
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
LOG_DBG("BMC", "Beginning content opf pass");
|
||||||
|
|
||||||
// Open spine file for writing
|
// Open spine file for writing
|
||||||
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endContentOpfPass() {
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
@@ -38,12 +38,12 @@ bool BookMetadataCache::endContentOpfPass() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::beginTocPass() {
|
bool BookMetadataCache::beginTocPass() {
|
||||||
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
LOG_DBG("BMC", "Beginning toc pass");
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ bool BookMetadataCache::beginTocPass() {
|
|||||||
});
|
});
|
||||||
spineFile.seek(0);
|
spineFile.seek(0);
|
||||||
useSpineHrefIndex = true;
|
useSpineHrefIndex = true;
|
||||||
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
|
LOG_DBG("BMC", "Using fast index for %d spine items", spineCount);
|
||||||
} else {
|
} else {
|
||||||
useSpineHrefIndex = false;
|
useSpineHrefIndex = false;
|
||||||
}
|
}
|
||||||
@@ -87,27 +87,27 @@ bool BookMetadataCache::endTocPass() {
|
|||||||
|
|
||||||
bool BookMetadataCache::endWrite() {
|
bool BookMetadataCache::endWrite() {
|
||||||
if (!buildMode) {
|
if (!buildMode) {
|
||||||
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
|
LOG_DBG("BMC", "endWrite called but not in build mode");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMode = false;
|
buildMode = false;
|
||||||
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||||
// Open all three files, writing to meta, reading from spine and toc
|
// Open all three files, writing to meta, reading from spine and toc
|
||||||
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -167,7 +167,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
ZipFile zip(epubPath);
|
ZipFile zip(epubPath);
|
||||||
// Pre-open zip file to speed up size calculations
|
// Pre-open zip file to speed up size calculations
|
||||||
if (!zip.open()) {
|
if (!zip.open()) {
|
||||||
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis());
|
LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
@@ -185,7 +185,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
bool useBatchSizes = false;
|
bool useBatchSizes = false;
|
||||||
|
|
||||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||||
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
|
LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
|
||||||
|
|
||||||
std::vector<ZipFile::SizeTarget> targets;
|
std::vector<ZipFile::SizeTarget> targets;
|
||||||
targets.reserve(spineCount);
|
targets.reserve(spineCount);
|
||||||
@@ -208,7 +208,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
|
|
||||||
spineSizes.resize(spineCount, 0);
|
spineSizes.resize(spineCount, 0);
|
||||||
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||||
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
|
LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", matched, spineCount);
|
||||||
|
|
||||||
targets.clear();
|
targets.clear();
|
||||||
targets.shrink_to_fit();
|
targets.shrink_to_fit();
|
||||||
@@ -227,9 +227,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||||
// Logging here is for debugging
|
// Logging here is for debugging
|
||||||
if (spineEntry.tocIndex == -1) {
|
if (spineEntry.tocIndex == -1) {
|
||||||
Serial.printf(
|
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
|
||||||
"[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s, using title from last section\n",
|
spineEntry.href.c_str());
|
||||||
millis(), i, spineEntry.href.c_str());
|
|
||||||
spineEntry.tocIndex = lastSpineTocIndex;
|
spineEntry.tocIndex = lastSpineTocIndex;
|
||||||
}
|
}
|
||||||
lastSpineTocIndex = spineEntry.tocIndex;
|
lastSpineTocIndex = spineEntry.tocIndex;
|
||||||
@@ -240,13 +239,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
if (itemSize == 0) {
|
if (itemSize == 0) {
|
||||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,16 +269,16 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
LOG_DBG("BMC", "Successfully built book.bin");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::cleanupTmpFiles() const {
|
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||||
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) {
|
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||||
SdMan.remove((cachePath + tmpSpineBinFile).c_str());
|
Storage.remove((cachePath + tmpSpineBinFile).c_str());
|
||||||
}
|
}
|
||||||
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) {
|
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||||
SdMan.remove((cachePath + tmpTocBinFile).c_str());
|
Storage.remove((cachePath + tmpTocBinFile).c_str());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -306,7 +305,7 @@ uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) c
|
|||||||
// this is because in this function we're marking positions of the items
|
// this is because in this function we're marking positions of the items
|
||||||
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||||
if (!buildMode || !spineFile) {
|
if (!buildMode || !spineFile) {
|
||||||
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
|
LOG_DBG("BMC", "createSpineEntry called but not in build mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +317,7 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
|||||||
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||||
const uint8_t level) {
|
const uint8_t level) {
|
||||||
if (!buildMode || !tocFile || !spineFile) {
|
if (!buildMode || !tocFile || !spineFile) {
|
||||||
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
|
LOG_DBG("BMC", "createTocEntry called but not in build mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +339,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spineIndex == -1) {
|
if (spineIndex == -1) {
|
||||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
spineFile.seek(0);
|
spineFile.seek(0);
|
||||||
@@ -352,7 +351,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (spineIndex == -1) {
|
if (spineIndex == -1) {
|
||||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,14 +363,14 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
bool BookMetadataCache::load() {
|
bool BookMetadataCache::load() {
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(bookFile, version);
|
serialization::readPod(bookFile, version);
|
||||||
if (version != BOOK_CACHE_VERSION) {
|
if (version != BOOK_CACHE_VERSION) {
|
||||||
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
|
LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -387,18 +386,18 @@ bool BookMetadataCache::load() {
|
|||||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
|
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||||
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,12 +411,12 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
|
|||||||
|
|
||||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||||
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
LOG_ERR("BMC", "getTocEntry index %d out of range", index);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
@@ -25,6 +25,29 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
|||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
// Images don't use fontId or text rendering
|
||||||
|
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PageImage::serialize(FsFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
|
||||||
|
// serialize ImageBlock
|
||||||
|
return imageBlock->serialize(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||||
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
serialization::readPod(file, xPos);
|
||||||
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
|
auto ib = ImageBlock::deserialize(file);
|
||||||
|
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||||
|
}
|
||||||
|
|
||||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
for (auto& element : elements) {
|
for (auto& element : elements) {
|
||||||
element->render(renderer, fontId, xOffset, yOffset);
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
@@ -36,8 +59,9 @@ bool Page::serialize(FsFile& file) const {
|
|||||||
serialization::writePod(file, count);
|
serialization::writePod(file, count);
|
||||||
|
|
||||||
for (const auto& el : elements) {
|
for (const auto& el : elements) {
|
||||||
// Only PageLine exists currently
|
// Use getTag() method to determine type
|
||||||
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||||
|
|
||||||
if (!el->serialize(file)) {
|
if (!el->serialize(file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -59,8 +83,11 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
|||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(file);
|
auto pl = PageLine::deserialize(file);
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
|
} else if (tag == TAG_PageImage) {
|
||||||
|
auto pi = PageImage::deserialize(file);
|
||||||
|
page->elements.push_back(std::move(pi));
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "blocks/ImageBlock.h"
|
||||||
#include "blocks/TextBlock.h"
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
enum PageElementTag : uint8_t {
|
enum PageElementTag : uint8_t {
|
||||||
TAG_PageLine = 1,
|
TAG_PageLine = 1,
|
||||||
|
TAG_PageImage = 2, // New tag
|
||||||
};
|
};
|
||||||
|
|
||||||
// represents something that has been added to a page
|
// represents something that has been added to a page
|
||||||
@@ -19,6 +22,7 @@ class PageElement {
|
|||||||
virtual ~PageElement() = default;
|
virtual ~PageElement() = default;
|
||||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||||
virtual bool serialize(FsFile& file) = 0;
|
virtual bool serialize(FsFile& file) = 0;
|
||||||
|
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||||
};
|
};
|
||||||
|
|
||||||
// a line from a block element
|
// a line from a block element
|
||||||
@@ -30,9 +34,23 @@ class PageLine final : public PageElement {
|
|||||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
bool serialize(FsFile& file) override;
|
bool serialize(FsFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// New PageImage class
|
||||||
|
class PageImage final : public PageElement {
|
||||||
|
std::shared_ptr<ImageBlock> imageBlock;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
bool serialize(FsFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
|
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||||
|
};
|
||||||
|
|
||||||
class Page {
|
class Page {
|
||||||
public:
|
public:
|
||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
@@ -40,4 +58,10 @@ class Page {
|
|||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
bool serialize(FsFile& file) const;
|
bool serialize(FsFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||||
|
|
||||||
|
// Check if page contains any images (used to force full refresh)
|
||||||
|
bool hasImages() const {
|
||||||
|
return std::any_of(elements.begin(), elements.end(),
|
||||||
|
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ void stripSoftHyphensInPlace(std::string& word) {
|
|||||||
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
||||||
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
||||||
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
||||||
|
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
|
||||||
|
return renderer.getSpaceWidth(fontId);
|
||||||
|
}
|
||||||
const bool hasSoftHyphen = containsSoftHyphen(word);
|
const bool hasSoftHyphen = containsSoftHyphen(word);
|
||||||
if (!hasSoftHyphen && !appendHyphen) {
|
if (!hasSoftHyphen && !appendHyphen) {
|
||||||
return renderer.getTextWidth(fontId, word.c_str(), style);
|
return renderer.getTextWidth(fontId, word.c_str(), style);
|
||||||
@@ -49,11 +52,17 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) {
|
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
||||||
|
const bool attachToPrevious) {
|
||||||
if (word.empty()) return;
|
if (word.empty()) return;
|
||||||
|
|
||||||
words.push_back(std::move(word));
|
words.push_back(std::move(word));
|
||||||
wordStyles.push_back(fontStyle);
|
EpdFontFamily::Style combinedStyle = fontStyle;
|
||||||
|
if (underline) {
|
||||||
|
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
|
||||||
|
}
|
||||||
|
wordStyles.push_back(combinedStyle);
|
||||||
|
wordContinues.push_back(attachToPrevious);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consumes data to minimize memory usage
|
// Consumes data to minimize memory usage
|
||||||
@@ -70,17 +79,21 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
const int pageWidth = viewportWidth;
|
const int pageWidth = viewportWidth;
|
||||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||||
|
|
||||||
|
// 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;
|
std::vector<size_t> lineBreakIndices;
|
||||||
if (hyphenationEnabled) {
|
if (hyphenationEnabled) {
|
||||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
|
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||||
} else {
|
} else {
|
||||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
|
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||||
}
|
}
|
||||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
for (size_t i = 0; i < lineCount; ++i) {
|
for (size_t i = 0; i < lineCount; ++i) {
|
||||||
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
|
extractLine(i, pageWidth, spaceWidth, wordWidths, continuesVec, lineBreakIndices, processLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,15 +117,26 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||||
const int spaceWidth, std::vector<uint16_t>& wordWidths) {
|
const int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||||
|
std::vector<bool>& continuesVec) {
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||||
|
const int firstLineIndent =
|
||||||
|
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
|
? blockStyle.textIndent
|
||||||
|
: 0;
|
||||||
|
|
||||||
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
||||||
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
||||||
while (wordWidths[i] > pageWidth) {
|
// First word needs to fit in reduced width if there's an indent
|
||||||
if (!hyphenateWordAtIndex(i, pageWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
|
const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||||
|
while (wordWidths[i] > effectiveWidth) {
|
||||||
|
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true,
|
||||||
|
&continuesVec)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,22 +154,31 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
|||||||
ans[totalWordCount - 1] = totalWordCount - 1;
|
ans[totalWordCount - 1] = totalWordCount - 1;
|
||||||
|
|
||||||
for (int i = totalWordCount - 2; i >= 0; --i) {
|
for (int i = totalWordCount - 2; i >= 0; --i) {
|
||||||
int currlen = -spaceWidth;
|
int currlen = 0;
|
||||||
dp[i] = MAX_COST;
|
dp[i] = MAX_COST;
|
||||||
|
|
||||||
for (size_t j = i; j < totalWordCount; ++j) {
|
// First line has reduced width due to text-indent
|
||||||
// Current line length: previous width + space + current word width
|
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||||
currlen += wordWidths[j] + spaceWidth;
|
|
||||||
|
|
||||||
if (currlen > pageWidth) {
|
for (size_t j = i; j < totalWordCount; ++j) {
|
||||||
|
// Add space before word j, unless it's the first word on the line or a continuation
|
||||||
|
const int gap = j > static_cast<size_t>(i) && !continuesVec[j] ? spaceWidth : 0;
|
||||||
|
currlen += wordWidths[j] + gap;
|
||||||
|
|
||||||
|
if (currlen > effectivePageWidth) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cannot break after word j if the next word attaches to it (continuation group)
|
||||||
|
if (j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
int cost;
|
int cost;
|
||||||
if (j == totalWordCount - 1) {
|
if (j == totalWordCount - 1) {
|
||||||
cost = 0; // Last line
|
cost = 0; // Last line
|
||||||
} else {
|
} else {
|
||||||
const int remainingSpace = pageWidth - currlen;
|
const int remainingSpace = effectivePageWidth - currlen;
|
||||||
// Use long long for the square to prevent overflow
|
// Use long long for the square to prevent overflow
|
||||||
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
|
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
|
||||||
|
|
||||||
@@ -200,7 +233,11 @@ void ParsedText::applyParagraphIndent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
|
if (blockStyle.textIndentDefined) {
|
||||||
|
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
|
||||||
|
// The actual indent positioning is handled in extractLine()
|
||||||
|
} else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) {
|
||||||
|
// No CSS text-indent defined - use EmSpace fallback for visual indent
|
||||||
words.front().insert(0, "\xe2\x80\x83");
|
words.front().insert(0, "\xe2\x80\x83");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,33 +245,45 @@ void ParsedText::applyParagraphIndent() {
|
|||||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||||
const int pageWidth, const int spaceWidth,
|
const int pageWidth, const int spaceWidth,
|
||||||
std::vector<uint16_t>& wordWidths) {
|
std::vector<uint16_t>& wordWidths,
|
||||||
|
std::vector<bool>& continuesVec) {
|
||||||
|
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||||
|
const int firstLineIndent =
|
||||||
|
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
|
? blockStyle.textIndent
|
||||||
|
: 0;
|
||||||
|
|
||||||
std::vector<size_t> lineBreakIndices;
|
std::vector<size_t> lineBreakIndices;
|
||||||
size_t currentIndex = 0;
|
size_t currentIndex = 0;
|
||||||
|
bool isFirstLine = true;
|
||||||
|
|
||||||
while (currentIndex < wordWidths.size()) {
|
while (currentIndex < wordWidths.size()) {
|
||||||
const size_t lineStart = currentIndex;
|
const size_t lineStart = currentIndex;
|
||||||
int lineWidth = 0;
|
int lineWidth = 0;
|
||||||
|
|
||||||
|
// First line has reduced width due to text-indent
|
||||||
|
const int effectivePageWidth = isFirstLine ? pageWidth - firstLineIndent : pageWidth;
|
||||||
|
|
||||||
// Consume as many words as possible for current line, splitting when prefixes fit
|
// Consume as many words as possible for current line, splitting when prefixes fit
|
||||||
while (currentIndex < wordWidths.size()) {
|
while (currentIndex < wordWidths.size()) {
|
||||||
const bool isFirstWord = currentIndex == lineStart;
|
const bool isFirstWord = currentIndex == lineStart;
|
||||||
const int spacing = isFirstWord ? 0 : spaceWidth;
|
const int spacing = isFirstWord || continuesVec[currentIndex] ? 0 : spaceWidth;
|
||||||
const int candidateWidth = spacing + wordWidths[currentIndex];
|
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||||
|
|
||||||
// Word fits on current line
|
// Word fits on current line
|
||||||
if (lineWidth + candidateWidth <= pageWidth) {
|
if (lineWidth + candidateWidth <= effectivePageWidth) {
|
||||||
lineWidth += candidateWidth;
|
lineWidth += candidateWidth;
|
||||||
++currentIndex;
|
++currentIndex;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word would overflow — try to split based on hyphenation points
|
// Word would overflow — try to split based on hyphenation points
|
||||||
const int availableWidth = pageWidth - lineWidth - spacing;
|
const int availableWidth = effectivePageWidth - lineWidth - spacing;
|
||||||
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
||||||
|
|
||||||
if (availableWidth > 0 &&
|
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
||||||
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
allowFallbackBreaks, &continuesVec)) {
|
||||||
// Prefix now fits; append it to this line and move to next line
|
// Prefix now fits; append it to this line and move to next line
|
||||||
lineWidth += spacing + wordWidths[currentIndex];
|
lineWidth += spacing + wordWidths[currentIndex];
|
||||||
++currentIndex;
|
++currentIndex;
|
||||||
@@ -249,7 +298,14 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't break before a continuation word (e.g., orphaned "?" after "question").
|
||||||
|
// Backtrack to the start of the continuation group so the whole group moves to the next line.
|
||||||
|
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
|
||||||
|
--currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
lineBreakIndices.push_back(currentIndex);
|
lineBreakIndices.push_back(currentIndex);
|
||||||
|
isFirstLine = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lineBreakIndices;
|
return lineBreakIndices;
|
||||||
@@ -259,7 +315,7 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
|||||||
// available width.
|
// available width.
|
||||||
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
||||||
const int fontId, std::vector<uint16_t>& wordWidths,
|
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.
|
// Guard against invalid indices or zero available width before attempting to split.
|
||||||
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -314,12 +370,28 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
|||||||
wordIt->push_back('-');
|
wordIt->push_back('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the remainder word (with matching style) directly after the prefix.
|
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
|
||||||
auto insertWordIt = std::next(wordIt);
|
auto insertWordIt = std::next(wordIt);
|
||||||
auto insertStyleIt = std::next(styleIt);
|
auto insertStyleIt = std::next(styleIt);
|
||||||
words.insert(insertWordIt, remainder);
|
words.insert(insertWordIt, remainder);
|
||||||
wordStyles.insert(insertStyleIt, style);
|
wordStyles.insert(insertStyleIt, style);
|
||||||
|
|
||||||
|
// The remainder inherits whatever continuation status the original word had with the word after it.
|
||||||
|
// Find the continues entry for the original word and insert the remainder's entry after it.
|
||||||
|
auto continuesIt = wordContinues.begin();
|
||||||
|
std::advance(continuesIt, wordIndex);
|
||||||
|
const bool originalContinuedToNext = *continuesIt;
|
||||||
|
// The original word (now prefix) does NOT continue to remainder (hyphen separates them)
|
||||||
|
*continuesIt = false;
|
||||||
|
const auto insertContinuesIt = std::next(continuesIt);
|
||||||
|
wordContinues.insert(insertContinuesIt, originalContinuedToNext);
|
||||||
|
|
||||||
|
// 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.
|
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||||
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
||||||
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
|
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
|
||||||
@@ -328,49 +400,76 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||||
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||||
|
const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||||
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||||
|
|
||||||
// Calculate total word width for this line
|
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||||
|
const bool isFirstLine = breakIndex == 0;
|
||||||
|
const int firstLineIndent =
|
||||||
|
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
|
? blockStyle.textIndent
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Calculate total word width for this line and count actual word gaps
|
||||||
|
// (continuation words attach to previous word with no gap)
|
||||||
int lineWordWidthSum = 0;
|
int lineWordWidthSum = 0;
|
||||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
size_t actualGapCount = 0;
|
||||||
lineWordWidthSum += wordWidths[i];
|
|
||||||
|
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||||
|
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
||||||
|
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||||
|
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||||
|
actualGapCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate spacing
|
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||||
const int spareSpace = pageWidth - lineWordWidthSum;
|
const int effectivePageWidth = pageWidth - firstLineIndent;
|
||||||
|
const int spareSpace = effectivePageWidth - lineWordWidthSum;
|
||||||
|
|
||||||
int spacing = spaceWidth;
|
int spacing = spaceWidth;
|
||||||
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
// For justified text, calculate spacing based on actual gap count
|
||||||
spacing = spareSpace / (lineWordCount - 1);
|
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1) {
|
||||||
|
spacing = spareSpace / static_cast<int>(actualGapCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate initial x position
|
// Calculate initial x position (first line starts at indent for left/justified text)
|
||||||
uint16_t xpos = 0;
|
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
||||||
if (style == TextBlock::RIGHT_ALIGN) {
|
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
||||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-calculate X positions for words
|
// Pre-calculate X positions for words
|
||||||
|
// Continuation words attach to the previous word with no space before them
|
||||||
std::list<uint16_t> lineXPos;
|
std::list<uint16_t> lineXPos;
|
||||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
|
||||||
const uint16_t currentWordWidth = wordWidths[i];
|
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||||
|
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
|
||||||
|
|
||||||
lineXPos.push_back(xpos);
|
lineXPos.push_back(xpos);
|
||||||
xpos += currentWordWidth + spacing;
|
|
||||||
|
// Add spacing after this word, unless the next word is a continuation
|
||||||
|
const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1];
|
||||||
|
|
||||||
|
xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterators always start at the beginning as we are moving content with splice below
|
// Iterators always start at the beginning as we are moving content with splice below
|
||||||
auto wordEndIt = words.begin();
|
auto wordEndIt = words.begin();
|
||||||
auto wordStyleEndIt = wordStyles.begin();
|
auto wordStyleEndIt = wordStyles.begin();
|
||||||
|
auto wordContinuesEndIt = wordContinues.begin();
|
||||||
std::advance(wordEndIt, lineWordCount);
|
std::advance(wordEndIt, lineWordCount);
|
||||||
std::advance(wordStyleEndIt, lineWordCount);
|
std::advance(wordStyleEndIt, lineWordCount);
|
||||||
|
std::advance(wordContinuesEndIt, lineWordCount);
|
||||||
|
|
||||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||||
std::list<std::string> lineWords;
|
std::list<std::string> lineWords;
|
||||||
@@ -378,11 +477,16 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
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) {
|
for (auto& word : lineWords) {
|
||||||
if (containsSoftHyphen(word)) {
|
if (containsSoftHyphen(word)) {
|
||||||
stripSoftHyphensInPlace(word);
|
stripSoftHyphensInPlace(word);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
processLine(
|
||||||
}
|
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "blocks/BlockStyle.h"
|
||||||
#include "blocks/TextBlock.h"
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
@@ -15,31 +16,34 @@ class GfxRenderer;
|
|||||||
class ParsedText {
|
class ParsedText {
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<EpdFontFamily::Style> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
TextBlock::Style style;
|
std::list<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||||
|
BlockStyle blockStyle;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
bool hyphenationEnabled;
|
bool hyphenationEnabled;
|
||||||
|
|
||||||
void applyParagraphIndent();
|
void applyParagraphIndent();
|
||||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
||||||
std::vector<uint16_t>& wordWidths);
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||||
|
std::vector<bool>& continuesVec);
|
||||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||||
|
std::vector<bool>* continuesVec = nullptr);
|
||||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||||
const std::vector<size_t>& lineBreakIndices,
|
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
|
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||||
const bool hyphenationEnabled = false)
|
const BlockStyle& blockStyle = BlockStyle())
|
||||||
: style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||||
~ParsedText() = default;
|
~ParsedText() = default;
|
||||||
|
|
||||||
void addWord(std::string word, EpdFontFamily::Style fontStyle);
|
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||||
void setStyle(const TextBlock::Style style) { this->style = style; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
TextBlock::Style getStyle() const { return style; }
|
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||||
size_t size() const { return words.size(); }
|
size_t size() const { return words.size(); }
|
||||||
bool isEmpty() const { return words.empty(); }
|
bool isEmpty() const { return words.empty(); }
|
||||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "Section.h"
|
#include "Section.h"
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
@@ -8,24 +9,24 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 10;
|
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||||
sizeof(uint32_t);
|
sizeof(uint32_t);
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t position = file.position();
|
const uint32_t position = file.position();
|
||||||
if (!page->serialize(file)) {
|
if (!page->serialize(file)) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
|
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
LOG_DBG("SCT", "Page %d processed", pageCount);
|
||||||
|
|
||||||
pageCount++;
|
pageCount++;
|
||||||
return position;
|
return position;
|
||||||
@@ -33,15 +34,16 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
|||||||
|
|
||||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||||
|
const bool embeddedStyle) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
LOG_DBG("SCT", "File not open for writing header");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||||
sizeof(uint32_t),
|
sizeof(embeddedStyle) + sizeof(uint32_t),
|
||||||
"Header size mismatch");
|
"Header size mismatch");
|
||||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||||
serialization::writePod(file, fontId);
|
serialization::writePod(file, fontId);
|
||||||
@@ -51,14 +53,15 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
|||||||
serialization::writePod(file, viewportWidth);
|
serialization::writePod(file, viewportWidth);
|
||||||
serialization::writePod(file, viewportHeight);
|
serialization::writePod(file, viewportHeight);
|
||||||
serialization::writePod(file, hyphenationEnabled);
|
serialization::writePod(file, hyphenationEnabled);
|
||||||
|
serialization::writePod(file, embeddedStyle);
|
||||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
||||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
|
||||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != SECTION_FILE_VERSION) {
|
if (version != SECTION_FILE_VERSION) {
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -79,6 +82,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
bool fileExtraParagraphSpacing;
|
bool fileExtraParagraphSpacing;
|
||||||
uint8_t fileParagraphAlignment;
|
uint8_t fileParagraphAlignment;
|
||||||
bool fileHyphenationEnabled;
|
bool fileHyphenationEnabled;
|
||||||
|
bool fileEmbeddedStyle;
|
||||||
serialization::readPod(file, fileFontId);
|
serialization::readPod(file, fileFontId);
|
||||||
serialization::readPod(file, fileLineCompression);
|
serialization::readPod(file, fileLineCompression);
|
||||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||||
@@ -86,13 +90,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
serialization::readPod(file, fileViewportWidth);
|
serialization::readPod(file, fileViewportWidth);
|
||||||
serialization::readPod(file, fileViewportHeight);
|
serialization::readPod(file, fileViewportHeight);
|
||||||
serialization::readPod(file, fileHyphenationEnabled);
|
serialization::readPod(file, fileHyphenationEnabled);
|
||||||
|
serialization::readPod(file, fileEmbeddedStyle);
|
||||||
|
|
||||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||||
hyphenationEnabled != fileHyphenationEnabled) {
|
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -100,39 +105,37 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
|
|
||||||
serialization::readPod(file, pageCount);
|
serialization::readPod(file, pageCount);
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||||
bool Section::clearCache() const {
|
bool Section::clearCache() const {
|
||||||
if (!SdMan.exists(filePath.c_str())) {
|
if (!Storage.exists(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.remove(filePath.c_str())) {
|
if (!Storage.remove(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
LOG_ERR("SCT", "Failed to clear cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
LOG_DBG("SCT", "Cache cleared successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
const std::function<void()>& progressSetupFn,
|
const std::function<void()>& popupFn) {
|
||||||
const std::function<void(int)>& progressFn) {
|
|
||||||
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
|
||||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
|
|
||||||
// Create cache directory if it doesn't exist
|
// Create cache directory if it doesn't exist
|
||||||
{
|
{
|
||||||
const auto sectionsDir = epub->getCachePath() + "/sections";
|
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||||
SdMan.mkdir(sectionsDir.c_str());
|
Storage.mkdir(sectionsDir.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry logic for SD card timing issues
|
// Retry logic for SD card timing issues
|
||||||
@@ -140,17 +143,17 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
uint32_t fileSize = 0;
|
uint32_t fileSize = 0;
|
||||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
||||||
if (attempt > 0) {
|
if (attempt > 0) {
|
||||||
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||||
delay(50); // Brief delay before retry
|
delay(50); // Brief delay before retry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any incomplete file from previous attempt before retrying
|
// Remove any incomplete file from previous attempt before retrying
|
||||||
if (SdMan.exists(tmpHtmlPath.c_str())) {
|
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
SdMan.remove(tmpHtmlPath.c_str());
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile tmpHtml;
|
FsFile tmpHtml;
|
||||||
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||||
@@ -158,44 +161,57 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
tmpHtml.close();
|
tmpHtml.close();
|
||||||
|
|
||||||
// If streaming failed, remove the incomplete file immediately
|
// If streaming failed, remove the incomplete file immediately
|
||||||
if (!success && SdMan.exists(tmpHtmlPath.c_str())) {
|
if (!success && Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
SdMan.remove(tmpHtmlPath.c_str());
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||||
|
|
||||||
// Only show progress bar for larger chapters where rendering overhead is worth it
|
if (!Storage.openFileForWrite("SCT", filePath, file)) {
|
||||||
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
|
||||||
progressSetupFn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled);
|
viewportHeight, hyphenationEnabled, embeddedStyle);
|
||||||
std::vector<uint32_t> lut = {};
|
std::vector<uint32_t> lut = {};
|
||||||
|
|
||||||
|
// Derive the content base directory and image cache path prefix for the parser
|
||||||
|
size_t lastSlash = localPath.find_last_of('/');
|
||||||
|
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||||
|
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||||
|
|
||||||
|
CssParser* cssParser = nullptr;
|
||||||
|
if (embeddedStyle) {
|
||||||
|
cssParser = epub->getCssParser();
|
||||||
|
if (cssParser) {
|
||||||
|
if (!cssParser->loadFromCache()) {
|
||||||
|
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
ChapterHtmlSlimParser visitor(
|
||||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled,
|
viewportHeight, hyphenationEnabled,
|
||||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||||
progressFn);
|
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
|
||||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
SdMan.remove(tmpHtmlPath.c_str());
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
LOG_ERR("SCT", "Failed to parse XML and build pages");
|
||||||
file.close();
|
file.close();
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
|
if (cssParser) {
|
||||||
|
cssParser->clear();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,9 +227,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasFailedLutRecords) {
|
if (hasFailedLutRecords) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
|
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||||
file.close();
|
file.close();
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,11 +238,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
serialization::writePod(file, pageCount);
|
serialization::writePod(file, pageCount);
|
||||||
serialization::writePod(file, lutOffset);
|
serialization::writePod(file, lutOffset);
|
||||||
file.close();
|
file.close();
|
||||||
|
if (cssParser) {
|
||||||
|
cssParser->clear();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class Section {
|
|||||||
FsFile file;
|
FsFile file;
|
||||||
|
|
||||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled);
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||||
|
bool embeddedStyle);
|
||||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -29,11 +30,10 @@ class Section {
|
|||||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
~Section() = default;
|
~Section() = default;
|
||||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled);
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
const std::function<void()>& progressSetupFn = nullptr,
|
const std::function<void()>& popupFn = nullptr);
|
||||||
const std::function<void(int)>& progressFn = nullptr);
|
|
||||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
|||||||
class Block {
|
class Block {
|
||||||
public:
|
public:
|
||||||
virtual ~Block() = default;
|
virtual ~Block() = default;
|
||||||
virtual void layout(GfxRenderer& renderer) = 0;
|
|
||||||
virtual BlockType getType() = 0;
|
virtual BlockType getType() = 0;
|
||||||
virtual bool isEmpty() = 0;
|
virtual bool isEmpty() = 0;
|
||||||
virtual void finish() {}
|
virtual void finish() {}
|
||||||
|
|||||||
97
lib/Epub/Epub/blocks/BlockStyle.h
Normal file
97
lib/Epub/Epub/blocks/BlockStyle.h
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/css/CssStyle.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockStyle - Block-level styling properties
|
||||||
|
*/
|
||||||
|
struct BlockStyle {
|
||||||
|
CssTextAlign alignment = CssTextAlign::Justify;
|
||||||
|
|
||||||
|
// Spacing (in pixels)
|
||||||
|
int16_t marginTop = 0;
|
||||||
|
int16_t marginBottom = 0;
|
||||||
|
int16_t marginLeft = 0;
|
||||||
|
int16_t marginRight = 0;
|
||||||
|
int16_t paddingTop = 0; // treated same as margin for rendering
|
||||||
|
int16_t paddingBottom = 0; // treated same as margin for rendering
|
||||||
|
int16_t paddingLeft = 0; // treated same as margin for rendering
|
||||||
|
int16_t paddingRight = 0; // treated same as margin for rendering
|
||||||
|
int16_t textIndent = 0;
|
||||||
|
bool textIndentDefined = false; // true if text-indent was explicitly set in CSS
|
||||||
|
bool textAlignDefined = false; // true if text-align was explicitly set in CSS
|
||||||
|
|
||||||
|
// Combined horizontal insets (margin + padding)
|
||||||
|
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
||||||
|
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
||||||
|
[[nodiscard]] int16_t totalHorizontalInset() const { return leftInset() + rightInset(); }
|
||||||
|
|
||||||
|
// Combine with another block style. Useful for parent -> child styles, where the child style should be
|
||||||
|
// applied on top of the parent's style to get the combined style.
|
||||||
|
BlockStyle getCombinedBlockStyle(const BlockStyle& child) const {
|
||||||
|
BlockStyle combinedBlockStyle;
|
||||||
|
|
||||||
|
combinedBlockStyle.marginTop = static_cast<int16_t>(child.marginTop + marginTop);
|
||||||
|
combinedBlockStyle.marginBottom = static_cast<int16_t>(child.marginBottom + marginBottom);
|
||||||
|
combinedBlockStyle.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
|
||||||
|
combinedBlockStyle.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
|
||||||
|
|
||||||
|
combinedBlockStyle.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
|
||||||
|
combinedBlockStyle.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
|
||||||
|
combinedBlockStyle.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
|
||||||
|
combinedBlockStyle.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
|
||||||
|
// Text indent: use child's if defined
|
||||||
|
if (child.textIndentDefined) {
|
||||||
|
combinedBlockStyle.textIndent = child.textIndent;
|
||||||
|
combinedBlockStyle.textIndentDefined = true;
|
||||||
|
} else {
|
||||||
|
combinedBlockStyle.textIndent = textIndent;
|
||||||
|
combinedBlockStyle.textIndentDefined = textIndentDefined;
|
||||||
|
}
|
||||||
|
// Text align: use child's if defined
|
||||||
|
if (child.textAlignDefined) {
|
||||||
|
combinedBlockStyle.alignment = child.alignment;
|
||||||
|
combinedBlockStyle.textAlignDefined = true;
|
||||||
|
} else {
|
||||||
|
combinedBlockStyle.alignment = alignment;
|
||||||
|
combinedBlockStyle.textAlignDefined = textAlignDefined;
|
||||||
|
}
|
||||||
|
return combinedBlockStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
||||||
|
// emSize is the current font line height, used for em/rem unit conversion
|
||||||
|
// paragraphAlignment is the user's paragraphAlignment setting preference
|
||||||
|
static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment,
|
||||||
|
const uint16_t viewportWidth = 0) {
|
||||||
|
BlockStyle blockStyle;
|
||||||
|
const float vw = viewportWidth;
|
||||||
|
// Resolve all CssLength values to pixels using the current font's em size and viewport width
|
||||||
|
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw);
|
||||||
|
|
||||||
|
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw);
|
||||||
|
|
||||||
|
// For textIndent: if it's a percentage we can't resolve (no viewport width),
|
||||||
|
// leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used
|
||||||
|
if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) {
|
||||||
|
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.textIndentDefined = true;
|
||||||
|
}
|
||||||
|
blockStyle.textAlignDefined = cssStyle.hasTextAlign();
|
||||||
|
// User setting overrides CSS, unless "Book's Style" alignment setting is selected
|
||||||
|
if (paragraphAlignment == CssTextAlign::None) {
|
||||||
|
blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify;
|
||||||
|
} else {
|
||||||
|
blockStyle.alignment = paragraphAlignment;
|
||||||
|
}
|
||||||
|
return blockStyle;
|
||||||
|
}
|
||||||
|
};
|
||||||
174
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
174
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#include "ImageBlock.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include "../converters/DitherUtils.h"
|
||||||
|
#include "../converters/ImageDecoderFactory.h"
|
||||||
|
|
||||||
|
// Cache file format:
|
||||||
|
// - uint16_t width
|
||||||
|
// - uint16_t height
|
||||||
|
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||||
|
|
||||||
|
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||||
|
: imagePath(imagePath), width(width), height(height) {}
|
||||||
|
|
||||||
|
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string getCachePath(const std::string& imagePath) {
|
||||||
|
// Replace extension with .pxc (pixel cache)
|
||||||
|
size_t dotPos = imagePath.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
return imagePath.substr(0, dotPos) + ".pxc";
|
||||||
|
}
|
||||||
|
return imagePath + ".pxc";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||||
|
int expectedHeight) {
|
||||||
|
FsFile cacheFile;
|
||||||
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedWidth, cachedHeight;
|
||||||
|
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||||
|
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||||
|
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||||
|
if (widthDiff > 1 || heightDiff > 1) {
|
||||||
|
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||||
|
expectedHeight);
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||||
|
expectedWidth = cachedWidth;
|
||||||
|
expectedHeight = cachedHeight;
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||||
|
|
||||||
|
// Read and render row by row to minimize memory usage
|
||||||
|
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||||
|
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||||
|
if (!rowBuffer) {
|
||||||
|
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < cachedHeight; row++) {
|
||||||
|
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||||
|
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||||
|
free(rowBuffer);
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int destY = y + row;
|
||||||
|
for (int col = 0; col < cachedWidth; col++) {
|
||||||
|
int byteIdx = col / 4;
|
||||||
|
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||||
|
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||||
|
|
||||||
|
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(rowBuffer);
|
||||||
|
cacheFile.close();
|
||||||
|
LOG_DBG("IMG", "Cache render complete");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||||
|
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||||
|
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Bounds check render position using logical screen dimensions
|
||||||
|
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||||
|
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||||
|
screenHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to render from cache first
|
||||||
|
std::string cachePath = getCachePath(imagePath);
|
||||||
|
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||||
|
return; // Successfully rendered from cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cache - need to decode the image
|
||||||
|
// Check if image file exists
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||||
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t fileSize = file.size();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (fileSize == 0) {
|
||||||
|
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
RenderConfig config;
|
||||||
|
config.x = x;
|
||||||
|
config.y = y;
|
||||||
|
config.maxWidth = width;
|
||||||
|
config.maxHeight = height;
|
||||||
|
config.useGrayscale = true;
|
||||||
|
config.useDithering = true;
|
||||||
|
config.performanceMode = false;
|
||||||
|
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||||
|
config.cachePath = cachePath; // Enable caching during decode
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||||
|
if (!decoder) {
|
||||||
|
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||||
|
|
||||||
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Decode successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::serialize(FsFile& file) {
|
||||||
|
serialization::writeString(file, imagePath);
|
||||||
|
serialization::writePod(file, width);
|
||||||
|
serialization::writePod(file, height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
||||||
|
std::string path;
|
||||||
|
serialization::readString(file, path);
|
||||||
|
int16_t w, h;
|
||||||
|
serialization::readPod(file, w);
|
||||||
|
serialization::readPod(file, h);
|
||||||
|
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||||
|
}
|
||||||
31
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
31
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Block.h"
|
||||||
|
|
||||||
|
class ImageBlock final : public Block {
|
||||||
|
public:
|
||||||
|
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
||||||
|
~ImageBlock() override = default;
|
||||||
|
|
||||||
|
const std::string& getImagePath() const { return imagePath; }
|
||||||
|
int16_t getWidth() const { return width; }
|
||||||
|
int16_t getHeight() const { return height; }
|
||||||
|
|
||||||
|
bool imageExists() const;
|
||||||
|
|
||||||
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
|
bool isEmpty() override { return false; }
|
||||||
|
|
||||||
|
void render(GfxRenderer& renderer, const int x, const int y);
|
||||||
|
bool serialize(FsFile& file);
|
||||||
|
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string imagePath;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
};
|
||||||
@@ -1,22 +1,46 @@
|
|||||||
#include "TextBlock.h"
|
#include "TextBlock.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
// Validate iterator bounds before rendering
|
// Validate iterator bounds before rendering
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||||
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
|
||||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto wordIt = words.begin();
|
auto wordIt = words.begin();
|
||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
auto wordXposIt = wordXpos.begin();
|
auto wordXposIt = wordXpos.begin();
|
||||||
|
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
for (size_t i = 0; i < words.size(); i++) {
|
||||||
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
const int wordX = *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 = *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;
|
||||||
|
|
||||||
|
int startX = wordX;
|
||||||
|
int underlineWidth = fullWordWidth;
|
||||||
|
|
||||||
|
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
|
||||||
|
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
||||||
|
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||||
|
const char* visiblePtr = w.c_str() + 3;
|
||||||
|
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83");
|
||||||
|
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||||
|
startX = wordX + prefixWidth;
|
||||||
|
underlineWidth = visibleWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
||||||
|
}
|
||||||
|
|
||||||
std::advance(wordIt, 1);
|
std::advance(wordIt, 1);
|
||||||
std::advance(wordStylesIt, 1);
|
std::advance(wordStylesIt, 1);
|
||||||
@@ -26,8 +50,8 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
|
|
||||||
bool TextBlock::serialize(FsFile& file) const {
|
bool TextBlock::serialize(FsFile& file) const {
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||||
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
|
||||||
words.size(), wordXpos.size(), wordStyles.size());
|
wordXpos.size(), wordStyles.size());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +61,19 @@ bool TextBlock::serialize(FsFile& file) const {
|
|||||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||||
|
|
||||||
// Block style
|
// Style (alignment + margins/padding/indent)
|
||||||
serialization::writePod(file, style);
|
serialization::writePod(file, blockStyle.alignment);
|
||||||
|
serialization::writePod(file, blockStyle.textAlignDefined);
|
||||||
|
serialization::writePod(file, blockStyle.marginTop);
|
||||||
|
serialization::writePod(file, blockStyle.marginBottom);
|
||||||
|
serialization::writePod(file, blockStyle.marginLeft);
|
||||||
|
serialization::writePod(file, blockStyle.marginRight);
|
||||||
|
serialization::writePod(file, blockStyle.paddingTop);
|
||||||
|
serialization::writePod(file, blockStyle.paddingBottom);
|
||||||
|
serialization::writePod(file, blockStyle.paddingLeft);
|
||||||
|
serialization::writePod(file, blockStyle.paddingRight);
|
||||||
|
serialization::writePod(file, blockStyle.textIndent);
|
||||||
|
serialization::writePod(file, blockStyle.textIndentDefined);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -48,14 +83,14 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontFamily::Style> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
Style style;
|
BlockStyle blockStyle;
|
||||||
|
|
||||||
// Word count
|
// Word count
|
||||||
serialization::readPod(file, wc);
|
serialization::readPod(file, wc);
|
||||||
|
|
||||||
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
||||||
if (wc > 10000) {
|
if (wc > 10000) {
|
||||||
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
|
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +102,20 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||||
|
|
||||||
// Block style
|
// Style (alignment + margins/padding/indent)
|
||||||
serialization::readPod(file, style);
|
serialization::readPod(file, blockStyle.alignment);
|
||||||
|
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||||
|
serialization::readPod(file, blockStyle.marginTop);
|
||||||
|
serialization::readPod(file, blockStyle.marginBottom);
|
||||||
|
serialization::readPod(file, blockStyle.marginLeft);
|
||||||
|
serialization::readPod(file, blockStyle.marginRight);
|
||||||
|
serialization::readPod(file, blockStyle.paddingTop);
|
||||||
|
serialization::readPod(file, blockStyle.paddingBottom);
|
||||||
|
serialization::readPod(file, blockStyle.paddingLeft);
|
||||||
|
serialization::readPod(file, blockStyle.paddingRight);
|
||||||
|
serialization::readPod(file, blockStyle.textIndent);
|
||||||
|
serialization::readPod(file, blockStyle.textIndentDefined);
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
return std::unique_ptr<TextBlock>(
|
||||||
|
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "Block.h"
|
#include "Block.h"
|
||||||
|
#include "BlockStyle.h"
|
||||||
|
|
||||||
// Represents a line of text on a page
|
// Represents a line of text on a page
|
||||||
class TextBlock final : public Block {
|
class TextBlock final : public Block {
|
||||||
public:
|
|
||||||
enum Style : uint8_t {
|
|
||||||
JUSTIFIED = 0,
|
|
||||||
LEFT_ALIGN = 1,
|
|
||||||
CENTER_ALIGN = 2,
|
|
||||||
RIGHT_ALIGN = 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontFamily::Style> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
Style style;
|
BlockStyle blockStyle;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||||
std::list<EpdFontFamily::Style> word_styles, const Style style)
|
std::list<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||||
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
: words(std::move(words)),
|
||||||
|
wordXpos(std::move(word_xpos)),
|
||||||
|
wordStyles(std::move(word_styles)),
|
||||||
|
blockStyle(blockStyle) {}
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
void setStyle(const Style style) { this->style = style; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
Style getStyle() const { return style; }
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool isEmpty() override { return words.empty(); }
|
||||||
void layout(GfxRenderer& renderer) override {};
|
|
||||||
// given a renderer works out where to break the words into lines
|
// given a renderer works out where to break the words into lines
|
||||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
BlockType getType() override { return TEXT_BLOCK; }
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
|
|||||||
40
lib/Epub/Epub/converters/DitherUtils.h
Normal file
40
lib/Epub/Epub/converters/DitherUtils.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// 4x4 Bayer matrix for ordered dithering
|
||||||
|
inline const uint8_t bayer4x4[4][4] = {
|
||||||
|
{0, 8, 2, 10},
|
||||||
|
{12, 4, 14, 6},
|
||||||
|
{3, 11, 1, 9},
|
||||||
|
{15, 7, 13, 5},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply Bayer dithering and quantize to 4 levels (0-3)
|
||||||
|
// Stateless - works correctly with any pixel processing order
|
||||||
|
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||||
|
int bayer = bayer4x4[y & 3][x & 3];
|
||||||
|
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
|
||||||
|
|
||||||
|
int adjusted = gray + dither;
|
||||||
|
if (adjusted < 0) adjusted = 0;
|
||||||
|
if (adjusted > 255) adjusted = 255;
|
||||||
|
|
||||||
|
if (adjusted < 64) return 0;
|
||||||
|
if (adjusted < 128) return 1;
|
||||||
|
if (adjusted < 192) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a pixel respecting the current render mode for grayscale support
|
||||||
|
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
|
||||||
|
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
|
||||||
|
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
|
||||||
|
renderer.drawPixel(x, y, true);
|
||||||
|
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
|
||||||
|
renderer.drawPixel(x, y, false);
|
||||||
|
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
|
||||||
|
renderer.drawPixel(x, y, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
42
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#include "ImageDecoderFactory.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "JpegToFramebufferConverter.h"
|
||||||
|
#include "PngToFramebufferConverter.h"
|
||||||
|
|
||||||
|
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||||
|
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||||
|
std::string ext = imagePath;
|
||||||
|
size_t dotPos = ext.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
ext = ext.substr(dotPos);
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ext = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||||
|
if (!jpegDecoder) {
|
||||||
|
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||||
|
}
|
||||||
|
return jpegDecoder.get();
|
||||||
|
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||||
|
if (!pngDecoder) {
|
||||||
|
pngDecoder.reset(new PngToFramebufferConverter());
|
||||||
|
}
|
||||||
|
return pngDecoder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||||
20
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
20
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class JpegToFramebufferConverter;
|
||||||
|
class PngToFramebufferConverter;
|
||||||
|
|
||||||
|
class ImageDecoderFactory {
|
||||||
|
public:
|
||||||
|
// Returns non-owning pointer - factory owns the decoder lifetime
|
||||||
|
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
|
||||||
|
static bool isFormatSupported(const std::string& imagePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
|
||||||
|
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
|
||||||
|
};
|
||||||
17
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
17
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||||
|
if (width * height > MAX_SOURCE_PIXELS) {
|
||||||
|
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
||||||
|
format.c_str(), MAX_SOURCE_PIXELS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||||
|
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||||
|
imagePath.c_str());
|
||||||
|
}
|
||||||
40
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
40
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
struct ImageDimensions {
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderConfig {
|
||||||
|
int x, y;
|
||||||
|
int maxWidth, maxHeight;
|
||||||
|
bool useGrayscale = true;
|
||||||
|
bool useDithering = true;
|
||||||
|
bool performanceMode = false;
|
||||||
|
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
|
||||||
|
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
virtual ~ImageToFramebufferDecoder() = default;
|
||||||
|
|
||||||
|
virtual bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) = 0;
|
||||||
|
|
||||||
|
virtual bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const = 0;
|
||||||
|
|
||||||
|
virtual const char* getFormatName() const = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Size validation helpers
|
||||||
|
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
||||||
|
|
||||||
|
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||||
|
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||||
|
};
|
||||||
297
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
297
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#include "JpegToFramebufferConverter.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <SdFat.h>
|
||||||
|
#include <picojpeg.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "DitherUtils.h"
|
||||||
|
#include "PixelCache.h"
|
||||||
|
|
||||||
|
struct JpegContext {
|
||||||
|
FsFile& file;
|
||||||
|
uint8_t buffer[512];
|
||||||
|
size_t bufferPos;
|
||||||
|
size_t bufferFilled;
|
||||||
|
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||||
|
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JpegContext context(file);
|
||||||
|
pjpeg_image_info_t imageInfo;
|
||||||
|
|
||||||
|
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (status != 0) {
|
||||||
|
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = imageInfo.m_width;
|
||||||
|
out.height = imageInfo.m_height;
|
||||||
|
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||||
|
const RenderConfig& config) {
|
||||||
|
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||||
|
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JpegContext context(file);
|
||||||
|
pjpeg_image_info_t imageInfo;
|
||||||
|
|
||||||
|
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||||
|
if (status != 0) {
|
||||||
|
LOG_ERR("JPG", "picojpeg init failed: %d", status);
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output dimensions
|
||||||
|
int destWidth, destHeight;
|
||||||
|
float scale;
|
||||||
|
|
||||||
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
|
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||||
|
destWidth = config.maxWidth;
|
||||||
|
destHeight = config.maxHeight;
|
||||||
|
scale = (float)destWidth / imageInfo.m_width;
|
||||||
|
} else {
|
||||||
|
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||||
|
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth)
|
||||||
|
? (float)config.maxWidth / imageInfo.m_width
|
||||||
|
: 1.0f;
|
||||||
|
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
|
||||||
|
? (float)config.maxHeight / imageInfo.m_height
|
||||||
|
: 1.0f;
|
||||||
|
scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (scale > 1.0f) scale = 1.0f;
|
||||||
|
|
||||||
|
destWidth = (int)(imageInfo.m_width * scale);
|
||||||
|
destHeight = (int)(imageInfo.m_height * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||||
|
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
||||||
|
|
||||||
|
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
|
||||||
|
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Allocate pixel cache if cachePath is provided
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching = !config.cachePath.empty();
|
||||||
|
if (caching) {
|
||||||
|
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||||
|
caching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mcuX = 0;
|
||||||
|
int mcuY = 0;
|
||||||
|
|
||||||
|
while (mcuY < imageInfo.m_MCUSPerCol) {
|
||||||
|
status = pjpeg_decode_mcu();
|
||||||
|
if (status == PJPG_NO_MORE_BLOCKS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (status != 0) {
|
||||||
|
LOG_ERR("JPG", "MCU decode failed: %d", status);
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source position in image coordinates
|
||||||
|
int srcStartX = mcuX * imageInfo.m_MCUWidth;
|
||||||
|
int srcStartY = mcuY * imageInfo.m_MCUHeight;
|
||||||
|
|
||||||
|
switch (imageInfo.m_scanType) {
|
||||||
|
case PJPG_GRAYSCALE:
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH1V1:
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH2V1:
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 16; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
int blockIndex = (col < 8) ? 0 : 1;
|
||||||
|
int pixelIndex = row * 8 + (col % 8);
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH1V2:
|
||||||
|
for (int row = 0; row < 16; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
int blockIndex = (row < 8) ? 0 : 1;
|
||||||
|
int pixelIndex = (row % 8) * 8 + col;
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH2V2:
|
||||||
|
for (int row = 0; row < 16; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 16; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
int blockX = (col < 8) ? 0 : 1;
|
||||||
|
int blockY = (row < 8) ? 0 : 1;
|
||||||
|
int blockIndex = blockY * 2 + blockX;
|
||||||
|
int pixelIndex = (row % 8) * 8 + (col % 8);
|
||||||
|
int blockOffset = blockIndex * 64;
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mcuX++;
|
||||||
|
if (mcuX >= imageInfo.m_MCUSPerRow) {
|
||||||
|
mcuX = 0;
|
||||||
|
mcuY++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "Decoding complete");
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Write cache file if caching was enabled
|
||||||
|
if (caching) {
|
||||||
|
cache.writeToFile(config.cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||||
|
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
||||||
|
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
|
||||||
|
|
||||||
|
if (context->bufferPos >= context->bufferFilled) {
|
||||||
|
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
|
||||||
|
if (readCount <= 0) {
|
||||||
|
*pBytes_actually_read = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
context->bufferFilled = readCount;
|
||||||
|
context->bufferPos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
|
||||||
|
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
|
||||||
|
|
||||||
|
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
|
||||||
|
context->bufferPos += bytesToCopy;
|
||||||
|
*pBytes_actually_read = bytesToCopy;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
|
std::string ext = extension;
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
return (ext == ".jpg" || ext == ".jpeg");
|
||||||
|
}
|
||||||
24
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
24
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||||
|
|
||||||
|
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||||
|
|
||||||
|
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||||
|
return getDimensionsStatic(imagePath, dims);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool supportsFormat(const std::string& extension);
|
||||||
|
const char* getFormatName() const override { return "JPEG"; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||||
|
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||||
|
};
|
||||||
82
lib/Epub/Epub/converters/PixelCache.h
Normal file
82
lib/Epub/Epub/converters/PixelCache.h
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Cache buffer for storing 2-bit pixels (4 levels) during decode.
|
||||||
|
// Packs 4 pixels per byte, MSB first.
|
||||||
|
struct PixelCache {
|
||||||
|
uint8_t* buffer;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int bytesPerRow;
|
||||||
|
int originX; // config.x - to convert screen coords to cache coords
|
||||||
|
int originY; // config.y
|
||||||
|
|
||||||
|
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
|
||||||
|
PixelCache(const PixelCache&) = delete;
|
||||||
|
PixelCache& operator=(const PixelCache&) = delete;
|
||||||
|
|
||||||
|
static constexpr size_t MAX_CACHE_BYTES = 256 * 1024; // 256KB limit for embedded targets
|
||||||
|
|
||||||
|
bool allocate(int w, int h, int ox, int oy) {
|
||||||
|
width = w;
|
||||||
|
height = h;
|
||||||
|
originX = ox;
|
||||||
|
originY = oy;
|
||||||
|
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||||
|
size_t bufferSize = (size_t)bytesPerRow * h;
|
||||||
|
if (bufferSize > MAX_CACHE_BYTES) {
|
||||||
|
LOG_ERR("IMG", "Cache buffer too large: %d bytes for %dx%d (limit %d)", bufferSize, w, h, MAX_CACHE_BYTES);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buffer = (uint8_t*)malloc(bufferSize);
|
||||||
|
if (buffer) {
|
||||||
|
memset(buffer, 0, bufferSize);
|
||||||
|
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
|
||||||
|
}
|
||||||
|
return buffer != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPixel(int screenX, int screenY, uint8_t value) {
|
||||||
|
if (!buffer) return;
|
||||||
|
int localX = screenX - originX;
|
||||||
|
int localY = screenY - originY;
|
||||||
|
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
|
||||||
|
|
||||||
|
int byteIdx = localY * bytesPerRow + localX / 4;
|
||||||
|
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||||
|
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeToFile(const std::string& cachePath) {
|
||||||
|
if (!buffer) return false;
|
||||||
|
|
||||||
|
FsFile cacheFile;
|
||||||
|
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||||
|
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t w = width;
|
||||||
|
uint16_t h = height;
|
||||||
|
cacheFile.write(&w, 2);
|
||||||
|
cacheFile.write(&h, 2);
|
||||||
|
cacheFile.write(buffer, bytesPerRow * height);
|
||||||
|
cacheFile.close();
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~PixelCache() {
|
||||||
|
if (buffer) {
|
||||||
|
free(buffer);
|
||||||
|
buffer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
362
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
362
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
#include "PngToFramebufferConverter.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <PNGdec.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
#include "DitherUtils.h"
|
||||||
|
#include "PixelCache.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Context struct passed through PNGdec callbacks to avoid global mutable state.
|
||||||
|
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||||
|
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
|
||||||
|
struct PngContext {
|
||||||
|
GfxRenderer* renderer;
|
||||||
|
const RenderConfig* config;
|
||||||
|
int screenWidth;
|
||||||
|
int screenHeight;
|
||||||
|
|
||||||
|
// Scaling state
|
||||||
|
float scale;
|
||||||
|
int srcWidth;
|
||||||
|
int srcHeight;
|
||||||
|
int dstWidth;
|
||||||
|
int dstHeight;
|
||||||
|
int lastDstY; // Track last rendered destination Y to avoid duplicates
|
||||||
|
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching;
|
||||||
|
|
||||||
|
uint8_t* grayLineBuffer;
|
||||||
|
|
||||||
|
PngContext()
|
||||||
|
: renderer(nullptr),
|
||||||
|
config(nullptr),
|
||||||
|
screenWidth(0),
|
||||||
|
screenHeight(0),
|
||||||
|
scale(1.0f),
|
||||||
|
srcWidth(0),
|
||||||
|
srcHeight(0),
|
||||||
|
dstWidth(0),
|
||||||
|
dstHeight(0),
|
||||||
|
lastDstY(-1),
|
||||||
|
caching(false),
|
||||||
|
grayLineBuffer(nullptr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// File I/O callbacks use pFile->fHandle to access the FsFile*,
|
||||||
|
// avoiding the need for global file state.
|
||||||
|
void* pngOpenWithHandle(const char* filename, int32_t* size) {
|
||||||
|
FsFile* f = new FsFile();
|
||||||
|
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
|
||||||
|
delete f;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
*size = f->size();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pngCloseWithHandle(void* handle) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(handle);
|
||||||
|
if (f) {
|
||||||
|
f->close();
|
||||||
|
delete f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||||
|
if (!f) return 0;
|
||||||
|
return f->read(pBuf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||||
|
if (!f) return -1;
|
||||||
|
return f->seek(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
|
||||||
|
// We heap-allocate it on demand rather than using a static instance, so this memory
|
||||||
|
// is only consumed while actually decoding/querying PNG images. This is critical on
|
||||||
|
// the ESP32-C3 where total RAM is ~320 KB.
|
||||||
|
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||||
|
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||||
|
|
||||||
|
// Convert entire source line to grayscale with alpha blending to white background.
|
||||||
|
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||||
|
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||||
|
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||||
|
switch (pixelType) {
|
||||||
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t* p = &pPixels[x * 3];
|
||||||
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_INDEXED:
|
||||||
|
if (palette) {
|
||||||
|
if (hasAlpha) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t idx = pPixels[x];
|
||||||
|
uint8_t* p = &palette[idx * 3];
|
||||||
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
uint8_t alpha = palette[768 + idx];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t* p = &palette[pPixels[x] * 3];
|
||||||
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_GRAY_ALPHA:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t gray = pPixels[x * 2];
|
||||||
|
uint8_t alpha = pPixels[x * 2 + 1];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t* p = &pPixels[x * 4];
|
||||||
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
uint8_t alpha = p[3];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
memset(grayLine, 128, width);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||||
|
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||||
|
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||||
|
|
||||||
|
int srcY = pDraw->y;
|
||||||
|
int srcWidth = ctx->srcWidth;
|
||||||
|
|
||||||
|
// Calculate destination Y with scaling
|
||||||
|
int dstY = (int)(srcY * ctx->scale);
|
||||||
|
|
||||||
|
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||||
|
if (dstY == ctx->lastDstY) return 1;
|
||||||
|
ctx->lastDstY = dstY;
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (dstY >= ctx->dstHeight) return 1;
|
||||||
|
|
||||||
|
int outY = ctx->config->y + dstY;
|
||||||
|
if (outY >= ctx->screenHeight) return 1;
|
||||||
|
|
||||||
|
// Convert entire source line to grayscale (improves cache locality)
|
||||||
|
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||||
|
pDraw->iHasAlpha);
|
||||||
|
|
||||||
|
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||||
|
int dstWidth = ctx->dstWidth;
|
||||||
|
int outXBase = ctx->config->x;
|
||||||
|
int screenWidth = ctx->screenWidth;
|
||||||
|
bool useDithering = ctx->config->useDithering;
|
||||||
|
bool caching = ctx->caching;
|
||||||
|
|
||||||
|
int srcX = 0;
|
||||||
|
int error = 0;
|
||||||
|
|
||||||
|
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||||
|
int outX = outXBase + dstX;
|
||||||
|
if (outX < screenWidth) {
|
||||||
|
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||||
|
|
||||||
|
uint8_t ditheredGray;
|
||||||
|
if (useDithering) {
|
||||||
|
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
ditheredGray = gray / 85;
|
||||||
|
if (ditheredGray > 3) ditheredGray = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||||
|
error += srcWidth;
|
||||||
|
while (error >= dstWidth) {
|
||||||
|
error -= dstWidth;
|
||||||
|
srcX++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||||
|
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PNG* png = new (std::nothrow) PNG();
|
||||||
|
if (!png) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = png->getWidth();
|
||||||
|
out.height = png->getHeight();
|
||||||
|
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||||
|
const RenderConfig& config) {
|
||||||
|
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||||
|
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
|
||||||
|
PNG* png = new (std::nothrow) PNG();
|
||||||
|
if (!png) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate PNG decoder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PngContext ctx;
|
||||||
|
ctx.renderer = &renderer;
|
||||||
|
ctx.config = &config;
|
||||||
|
ctx.screenWidth = renderer.getScreenWidth();
|
||||||
|
ctx.screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||||
|
pngDrawCallback);
|
||||||
|
if (rc != PNG_SUCCESS) {
|
||||||
|
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output dimensions
|
||||||
|
ctx.srcWidth = png->getWidth();
|
||||||
|
ctx.srcHeight = png->getHeight();
|
||||||
|
|
||||||
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
|
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||||
|
ctx.dstWidth = config.maxWidth;
|
||||||
|
ctx.dstHeight = config.maxHeight;
|
||||||
|
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||||
|
} else {
|
||||||
|
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||||
|
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||||
|
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||||
|
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||||
|
|
||||||
|
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||||
|
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||||
|
}
|
||||||
|
ctx.lastDstY = -1; // Reset row tracking
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||||
|
ctx.scale, png->getBpp());
|
||||||
|
|
||||||
|
if (png->getBpp() != 8) {
|
||||||
|
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||||
|
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||||
|
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||||
|
if (!ctx.grayLineBuffer) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate cache buffer using SCALED dimensions
|
||||||
|
ctx.caching = !config.cachePath.empty();
|
||||||
|
if (ctx.caching) {
|
||||||
|
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
|
||||||
|
ctx.caching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long decodeStart = millis();
|
||||||
|
rc = png->decode(&ctx, 0);
|
||||||
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
|
free(ctx.grayLineBuffer);
|
||||||
|
ctx.grayLineBuffer = nullptr;
|
||||||
|
|
||||||
|
if (rc != PNG_SUCCESS) {
|
||||||
|
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
|
||||||
|
|
||||||
|
// Write cache file if caching was enabled and buffer was allocated
|
||||||
|
if (ctx.caching) {
|
||||||
|
ctx.cache.writeToFile(config.cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
|
std::string ext = extension;
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
return (ext == ".png");
|
||||||
|
}
|
||||||
17
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
17
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class PngToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||||
|
|
||||||
|
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||||
|
|
||||||
|
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||||
|
return getDimensionsStatic(imagePath, dims);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool supportsFormat(const std::string& extension);
|
||||||
|
const char* getFormatName() const override { return "PNG"; }
|
||||||
|
};
|
||||||
766
lib/Epub/Epub/css/CssParser.cpp
Normal file
766
lib/Epub/Epub/css/CssParser.cpp
Normal file
@@ -0,0 +1,766 @@
|
|||||||
|
#include "CssParser.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Stack-allocated string buffer to avoid heap reallocations during parsing
|
||||||
|
// Provides string-like interface with fixed capacity
|
||||||
|
struct StackBuffer {
|
||||||
|
static constexpr size_t CAPACITY = 1024;
|
||||||
|
char data[CAPACITY];
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
void push_back(char c) {
|
||||||
|
if (len < CAPACITY - 1) {
|
||||||
|
data[len++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() { len = 0; }
|
||||||
|
bool empty() const { return len == 0; }
|
||||||
|
size_t size() const { return len; }
|
||||||
|
|
||||||
|
// Get string view of current content (zero-copy)
|
||||||
|
std::string_view view() const { return std::string_view(data, len); }
|
||||||
|
|
||||||
|
// Convert to string for passing to functions (single allocation)
|
||||||
|
std::string str() const { return std::string(data, len); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buffer size for reading CSS files
|
||||||
|
constexpr size_t READ_BUFFER_SIZE = 512;
|
||||||
|
|
||||||
|
// Maximum number of CSS rules to store in the selector map
|
||||||
|
// Prevents unbounded memory growth from pathological CSS files
|
||||||
|
constexpr size_t MAX_RULES = 1500;
|
||||||
|
|
||||||
|
// Minimum free heap required to apply CSS during rendering
|
||||||
|
// If below this threshold, we skip CSS to avoid display artifacts.
|
||||||
|
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
|
||||||
|
|
||||||
|
// Maximum length for a single selector string
|
||||||
|
// Prevents parsing of extremely long or malformed selectors
|
||||||
|
constexpr size_t MAX_SELECTOR_LENGTH = 256;
|
||||||
|
|
||||||
|
// Check if character is CSS whitespace
|
||||||
|
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
// String utilities implementation
|
||||||
|
|
||||||
|
std::string CssParser::normalized(const std::string& s) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(s.size());
|
||||||
|
|
||||||
|
bool inSpace = true; // Start true to skip leading space
|
||||||
|
for (const char c : s) {
|
||||||
|
if (isCssWhitespace(c)) {
|
||||||
|
if (!inSpace) {
|
||||||
|
result.push_back(' ');
|
||||||
|
inSpace = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
||||||
|
inSpace = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing space
|
||||||
|
if (!result.empty() && result.back() == ' ') {
|
||||||
|
result.pop_back();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CssParser::normalizedInto(const std::string& s, std::string& out) {
|
||||||
|
out.clear();
|
||||||
|
out.reserve(s.size());
|
||||||
|
|
||||||
|
bool inSpace = true; // Start true to skip leading space
|
||||||
|
for (const char c : s) {
|
||||||
|
if (isCssWhitespace(c)) {
|
||||||
|
if (!inSpace) {
|
||||||
|
out.push_back(' ');
|
||||||
|
inSpace = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
||||||
|
inSpace = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!out.empty() && out.back() == ' ') {
|
||||||
|
out.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> CssParser::splitOnChar(const std::string& s, const char delimiter) {
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
size_t start = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i <= s.size(); ++i) {
|
||||||
|
if (i == s.size() || s[i] == delimiter) {
|
||||||
|
std::string part = s.substr(start, i - start);
|
||||||
|
std::string trimmed = normalized(part);
|
||||||
|
if (!trimmed.empty()) {
|
||||||
|
parts.push_back(trimmed);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> CssParser::splitWhitespace(const std::string& s) {
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
size_t start = 0;
|
||||||
|
bool inWord = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i <= s.size(); ++i) {
|
||||||
|
const bool isSpace = i == s.size() || isCssWhitespace(s[i]);
|
||||||
|
if (isSpace && inWord) {
|
||||||
|
parts.push_back(s.substr(start, i - start));
|
||||||
|
inWord = false;
|
||||||
|
} else if (!isSpace && !inWord) {
|
||||||
|
start = i;
|
||||||
|
inWord = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property value interpreters
|
||||||
|
|
||||||
|
CssTextAlign CssParser::interpretAlignment(const std::string& val) {
|
||||||
|
const std::string v = normalized(val);
|
||||||
|
|
||||||
|
if (v == "left" || v == "start") return CssTextAlign::Left;
|
||||||
|
if (v == "right" || v == "end") return CssTextAlign::Right;
|
||||||
|
if (v == "center") return CssTextAlign::Center;
|
||||||
|
if (v == "justify") return CssTextAlign::Justify;
|
||||||
|
|
||||||
|
return CssTextAlign::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssFontStyle CssParser::interpretFontStyle(const std::string& val) {
|
||||||
|
const std::string v = normalized(val);
|
||||||
|
|
||||||
|
if (v == "italic" || v == "oblique") return CssFontStyle::Italic;
|
||||||
|
return CssFontStyle::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssFontWeight CssParser::interpretFontWeight(const std::string& val) {
|
||||||
|
const std::string v = normalized(val);
|
||||||
|
|
||||||
|
// Named values
|
||||||
|
if (v == "bold" || v == "bolder") return CssFontWeight::Bold;
|
||||||
|
if (v == "normal" || v == "lighter") return CssFontWeight::Normal;
|
||||||
|
|
||||||
|
// Numeric values: 100-900
|
||||||
|
// CSS spec: 400 = normal, 700 = bold
|
||||||
|
// We use: 0-400 = normal, 700+ = bold, 500-600 = normal (conservative)
|
||||||
|
char* endPtr = nullptr;
|
||||||
|
const long numericWeight = std::strtol(v.c_str(), &endPtr, 10);
|
||||||
|
|
||||||
|
// If we parsed a number and consumed the whole string
|
||||||
|
if (endPtr != v.c_str() && *endPtr == '\0') {
|
||||||
|
return numericWeight >= 700 ? CssFontWeight::Bold : CssFontWeight::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CssFontWeight::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssTextDecoration CssParser::interpretDecoration(const std::string& val) {
|
||||||
|
const std::string v = normalized(val);
|
||||||
|
|
||||||
|
// text-decoration can have multiple space-separated values
|
||||||
|
if (v.find("underline") != std::string::npos) {
|
||||||
|
return CssTextDecoration::Underline;
|
||||||
|
}
|
||||||
|
return CssTextDecoration::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssLength CssParser::interpretLength(const std::string& val) {
|
||||||
|
const std::string v = normalized(val);
|
||||||
|
if (v.empty()) return CssLength{};
|
||||||
|
|
||||||
|
// Find where the number ends
|
||||||
|
size_t unitStart = v.size();
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) {
|
||||||
|
const char c = v[i];
|
||||||
|
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||||
|
unitStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string numPart = v.substr(0, unitStart);
|
||||||
|
const std::string unitPart = v.substr(unitStart);
|
||||||
|
|
||||||
|
// Parse numeric value
|
||||||
|
char* endPtr = nullptr;
|
||||||
|
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
||||||
|
if (endPtr == numPart.c_str()) return CssLength{}; // No number parsed
|
||||||
|
|
||||||
|
// Determine unit type (preserve for deferred resolution)
|
||||||
|
auto unit = CssUnit::Pixels;
|
||||||
|
if (unitPart == "em") {
|
||||||
|
unit = CssUnit::Em;
|
||||||
|
} else if (unitPart == "rem") {
|
||||||
|
unit = CssUnit::Rem;
|
||||||
|
} else if (unitPart == "pt") {
|
||||||
|
unit = CssUnit::Points;
|
||||||
|
} else if (unitPart == "%") {
|
||||||
|
unit = CssUnit::Percent;
|
||||||
|
}
|
||||||
|
// px and unitless default to Pixels
|
||||||
|
|
||||||
|
return CssLength{numericValue, unit};
|
||||||
|
}
|
||||||
|
// Declaration parsing
|
||||||
|
|
||||||
|
void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf,
|
||||||
|
std::string& propValueBuf) {
|
||||||
|
const size_t colonPos = decl.find(':');
|
||||||
|
if (colonPos == std::string::npos || colonPos == 0) return;
|
||||||
|
|
||||||
|
normalizedInto(decl.substr(0, colonPos), propNameBuf);
|
||||||
|
normalizedInto(decl.substr(colonPos + 1), propValueBuf);
|
||||||
|
|
||||||
|
if (propNameBuf.empty() || propValueBuf.empty()) return;
|
||||||
|
|
||||||
|
if (propNameBuf == "text-align") {
|
||||||
|
style.textAlign = interpretAlignment(propValueBuf);
|
||||||
|
style.defined.textAlign = 1;
|
||||||
|
} else if (propNameBuf == "font-style") {
|
||||||
|
style.fontStyle = interpretFontStyle(propValueBuf);
|
||||||
|
style.defined.fontStyle = 1;
|
||||||
|
} else if (propNameBuf == "font-weight") {
|
||||||
|
style.fontWeight = interpretFontWeight(propValueBuf);
|
||||||
|
style.defined.fontWeight = 1;
|
||||||
|
} else if (propNameBuf == "text-decoration" || propNameBuf == "text-decoration-line") {
|
||||||
|
style.textDecoration = interpretDecoration(propValueBuf);
|
||||||
|
style.defined.textDecoration = 1;
|
||||||
|
} else if (propNameBuf == "text-indent") {
|
||||||
|
style.textIndent = interpretLength(propValueBuf);
|
||||||
|
style.defined.textIndent = 1;
|
||||||
|
} else if (propNameBuf == "margin-top") {
|
||||||
|
style.marginTop = interpretLength(propValueBuf);
|
||||||
|
style.defined.marginTop = 1;
|
||||||
|
} else if (propNameBuf == "margin-bottom") {
|
||||||
|
style.marginBottom = interpretLength(propValueBuf);
|
||||||
|
style.defined.marginBottom = 1;
|
||||||
|
} else if (propNameBuf == "margin-left") {
|
||||||
|
style.marginLeft = interpretLength(propValueBuf);
|
||||||
|
style.defined.marginLeft = 1;
|
||||||
|
} else if (propNameBuf == "margin-right") {
|
||||||
|
style.marginRight = interpretLength(propValueBuf);
|
||||||
|
style.defined.marginRight = 1;
|
||||||
|
} else if (propNameBuf == "margin") {
|
||||||
|
const auto values = splitWhitespace(propValueBuf);
|
||||||
|
if (!values.empty()) {
|
||||||
|
style.marginTop = interpretLength(values[0]);
|
||||||
|
style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop;
|
||||||
|
style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop;
|
||||||
|
style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight;
|
||||||
|
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
} else if (propNameBuf == "padding-top") {
|
||||||
|
style.paddingTop = interpretLength(propValueBuf);
|
||||||
|
style.defined.paddingTop = 1;
|
||||||
|
} else if (propNameBuf == "padding-bottom") {
|
||||||
|
style.paddingBottom = interpretLength(propValueBuf);
|
||||||
|
style.defined.paddingBottom = 1;
|
||||||
|
} else if (propNameBuf == "padding-left") {
|
||||||
|
style.paddingLeft = interpretLength(propValueBuf);
|
||||||
|
style.defined.paddingLeft = 1;
|
||||||
|
} else if (propNameBuf == "padding-right") {
|
||||||
|
style.paddingRight = interpretLength(propValueBuf);
|
||||||
|
style.defined.paddingRight = 1;
|
||||||
|
} else if (propNameBuf == "padding") {
|
||||||
|
const auto values = splitWhitespace(propValueBuf);
|
||||||
|
if (!values.empty()) {
|
||||||
|
style.paddingTop = interpretLength(values[0]);
|
||||||
|
style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop;
|
||||||
|
style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop;
|
||||||
|
style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight;
|
||||||
|
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
|
||||||
|
1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||||
|
CssStyle style;
|
||||||
|
std::string propNameBuf;
|
||||||
|
std::string propValueBuf;
|
||||||
|
|
||||||
|
size_t start = 0;
|
||||||
|
for (size_t i = 0; i <= declBlock.size(); ++i) {
|
||||||
|
if (i == declBlock.size() || declBlock[i] == ';') {
|
||||||
|
if (i > start) {
|
||||||
|
const size_t len = i - start;
|
||||||
|
std::string decl = declBlock.substr(start, len);
|
||||||
|
if (!decl.empty()) {
|
||||||
|
parseDeclarationIntoStyle(decl, style, propNameBuf, propValueBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule processing
|
||||||
|
|
||||||
|
void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style) {
|
||||||
|
// Check if we've reached the rule limit before processing
|
||||||
|
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||||
|
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comma-separated selectors
|
||||||
|
const auto selectors = splitOnChar(selectorGroup, ',');
|
||||||
|
|
||||||
|
for (const auto& sel : selectors) {
|
||||||
|
// Validate selector length before processing
|
||||||
|
if (sel.size() > MAX_SELECTOR_LENGTH) {
|
||||||
|
LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the selector
|
||||||
|
std::string key = normalized(sel);
|
||||||
|
if (key.empty()) continue;
|
||||||
|
|
||||||
|
// Skip if this would exceed the rule limit
|
||||||
|
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||||
|
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store or merge with existing
|
||||||
|
auto it = rulesBySelector_.find(key);
|
||||||
|
if (it != rulesBySelector_.end()) {
|
||||||
|
it->second.applyOver(style);
|
||||||
|
} else {
|
||||||
|
rulesBySelector_[key] = style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main parsing entry point
|
||||||
|
|
||||||
|
bool CssParser::loadFromStream(FsFile& source) {
|
||||||
|
if (!source) {
|
||||||
|
LOG_ERR("CSS", "Cannot read from invalid file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t totalRead = 0;
|
||||||
|
|
||||||
|
// Use stack-allocated buffers for parsing to avoid heap reallocations
|
||||||
|
StackBuffer selector;
|
||||||
|
StackBuffer declBuffer;
|
||||||
|
// Keep these as std::string since they're passed by reference to parseDeclarationIntoStyle
|
||||||
|
std::string propNameBuf;
|
||||||
|
std::string propValueBuf;
|
||||||
|
|
||||||
|
bool inComment = false;
|
||||||
|
bool maybeSlash = false;
|
||||||
|
bool prevStar = false;
|
||||||
|
|
||||||
|
bool inAtRule = false;
|
||||||
|
int atDepth = 0;
|
||||||
|
|
||||||
|
int bodyDepth = 0;
|
||||||
|
bool skippingRule = false;
|
||||||
|
CssStyle currentStyle;
|
||||||
|
|
||||||
|
auto handleChar = [&](const char c) {
|
||||||
|
if (inAtRule) {
|
||||||
|
if (c == '{') {
|
||||||
|
++atDepth;
|
||||||
|
} else if (c == '}') {
|
||||||
|
if (atDepth > 0) --atDepth;
|
||||||
|
if (atDepth == 0) inAtRule = false;
|
||||||
|
} else if (c == ';' && atDepth == 0) {
|
||||||
|
inAtRule = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyDepth == 0) {
|
||||||
|
if (selector.empty() && isCssWhitespace(c)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c == '@' && selector.empty()) {
|
||||||
|
inAtRule = true;
|
||||||
|
atDepth = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c == '{') {
|
||||||
|
bodyDepth = 1;
|
||||||
|
currentStyle = CssStyle{};
|
||||||
|
declBuffer.clear();
|
||||||
|
if (selector.size() > MAX_SELECTOR_LENGTH * 4) {
|
||||||
|
skippingRule = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selector.push_back(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bodyDepth > 0
|
||||||
|
if (c == '{') {
|
||||||
|
++bodyDepth;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c == '}') {
|
||||||
|
--bodyDepth;
|
||||||
|
if (bodyDepth == 0) {
|
||||||
|
if (!skippingRule && !declBuffer.empty()) {
|
||||||
|
parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf);
|
||||||
|
}
|
||||||
|
if (!skippingRule) {
|
||||||
|
processRuleBlockWithStyle(selector.str(), currentStyle);
|
||||||
|
}
|
||||||
|
selector.clear();
|
||||||
|
declBuffer.clear();
|
||||||
|
skippingRule = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bodyDepth > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!skippingRule) {
|
||||||
|
if (c == ';') {
|
||||||
|
if (!declBuffer.empty()) {
|
||||||
|
parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf);
|
||||||
|
declBuffer.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
declBuffer.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
char buffer[READ_BUFFER_SIZE];
|
||||||
|
while (source.available()) {
|
||||||
|
int bytesRead = source.read(buffer, sizeof(buffer));
|
||||||
|
if (bytesRead <= 0) break;
|
||||||
|
|
||||||
|
totalRead += static_cast<size_t>(bytesRead);
|
||||||
|
|
||||||
|
for (int i = 0; i < bytesRead; ++i) {
|
||||||
|
const char c = buffer[i];
|
||||||
|
|
||||||
|
if (inComment) {
|
||||||
|
if (prevStar && c == '/') {
|
||||||
|
inComment = false;
|
||||||
|
prevStar = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prevStar = c == '*';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybeSlash) {
|
||||||
|
if (c == '*') {
|
||||||
|
inComment = true;
|
||||||
|
maybeSlash = false;
|
||||||
|
prevStar = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
handleChar('/');
|
||||||
|
maybeSlash = false;
|
||||||
|
// fall through to process current char
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '/') {
|
||||||
|
maybeSlash = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChar(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybeSlash) {
|
||||||
|
handleChar('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CSS", "Parsed %zu rules from %zu bytes", rulesBySelector_.size(), totalRead);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style resolution
|
||||||
|
|
||||||
|
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
|
||||||
|
static bool lowHeapWarningLogged = false;
|
||||||
|
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
|
||||||
|
if (!lowHeapWarningLogged) {
|
||||||
|
lowHeapWarningLogged = true;
|
||||||
|
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
|
||||||
|
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
|
||||||
|
}
|
||||||
|
return CssStyle{};
|
||||||
|
}
|
||||||
|
CssStyle result;
|
||||||
|
const std::string tag = normalized(tagName);
|
||||||
|
|
||||||
|
// 1. Apply element-level style (lowest priority)
|
||||||
|
const auto tagIt = rulesBySelector_.find(tag);
|
||||||
|
if (tagIt != rulesBySelector_.end()) {
|
||||||
|
result.applyOver(tagIt->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Apply class styles (medium priority)
|
||||||
|
if (!classAttr.empty()) {
|
||||||
|
const auto classes = splitWhitespace(classAttr);
|
||||||
|
|
||||||
|
for (const auto& cls : classes) {
|
||||||
|
std::string classKey = "." + normalized(cls);
|
||||||
|
|
||||||
|
auto classIt = rulesBySelector_.find(classKey);
|
||||||
|
if (classIt != rulesBySelector_.end()) {
|
||||||
|
result.applyOver(classIt->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Apply element.class styles (higher priority)
|
||||||
|
for (const auto& cls : classes) {
|
||||||
|
std::string combinedKey = tag + "." + normalized(cls);
|
||||||
|
|
||||||
|
auto combinedIt = rulesBySelector_.find(combinedKey);
|
||||||
|
if (combinedIt != rulesBySelector_.end()) {
|
||||||
|
result.applyOver(combinedIt->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline style parsing (static - doesn't need rule database)
|
||||||
|
|
||||||
|
CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return parseDeclarations(styleValue); }
|
||||||
|
|
||||||
|
// Cache serialization
|
||||||
|
|
||||||
|
// Cache format version - increment when format changes
|
||||||
|
constexpr uint8_t CSS_CACHE_VERSION = 2;
|
||||||
|
constexpr char rulesCache[] = "/css_rules.cache";
|
||||||
|
|
||||||
|
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
||||||
|
|
||||||
|
bool CssParser::saveToCache() const {
|
||||||
|
if (cachePath.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write version
|
||||||
|
file.write(CSS_CACHE_VERSION);
|
||||||
|
|
||||||
|
// Write rule count
|
||||||
|
const auto ruleCount = static_cast<uint16_t>(rulesBySelector_.size());
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&ruleCount), sizeof(ruleCount));
|
||||||
|
|
||||||
|
// Write each rule: selector string + CssStyle fields
|
||||||
|
for (const auto& pair : rulesBySelector_) {
|
||||||
|
// Write selector string (length-prefixed)
|
||||||
|
const auto selectorLen = static_cast<uint16_t>(pair.first.size());
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&selectorLen), sizeof(selectorLen));
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(pair.first.data()), selectorLen);
|
||||||
|
|
||||||
|
// Write CssStyle fields (all are POD types)
|
||||||
|
const CssStyle& style = pair.second;
|
||||||
|
file.write(static_cast<uint8_t>(style.textAlign));
|
||||||
|
file.write(static_cast<uint8_t>(style.fontStyle));
|
||||||
|
file.write(static_cast<uint8_t>(style.fontWeight));
|
||||||
|
file.write(static_cast<uint8_t>(style.textDecoration));
|
||||||
|
|
||||||
|
// Write CssLength fields (value + unit)
|
||||||
|
auto writeLength = [&file](const CssLength& len) {
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&len.value), sizeof(len.value));
|
||||||
|
file.write(static_cast<uint8_t>(len.unit));
|
||||||
|
};
|
||||||
|
|
||||||
|
writeLength(style.textIndent);
|
||||||
|
writeLength(style.marginTop);
|
||||||
|
writeLength(style.marginBottom);
|
||||||
|
writeLength(style.marginLeft);
|
||||||
|
writeLength(style.marginRight);
|
||||||
|
writeLength(style.paddingTop);
|
||||||
|
writeLength(style.paddingBottom);
|
||||||
|
writeLength(style.paddingLeft);
|
||||||
|
writeLength(style.paddingRight);
|
||||||
|
|
||||||
|
// Write defined flags as uint16_t
|
||||||
|
uint16_t definedBits = 0;
|
||||||
|
if (style.defined.textAlign) definedBits |= 1 << 0;
|
||||||
|
if (style.defined.fontStyle) definedBits |= 1 << 1;
|
||||||
|
if (style.defined.fontWeight) definedBits |= 1 << 2;
|
||||||
|
if (style.defined.textDecoration) definedBits |= 1 << 3;
|
||||||
|
if (style.defined.textIndent) definedBits |= 1 << 4;
|
||||||
|
if (style.defined.marginTop) definedBits |= 1 << 5;
|
||||||
|
if (style.defined.marginBottom) definedBits |= 1 << 6;
|
||||||
|
if (style.defined.marginLeft) definedBits |= 1 << 7;
|
||||||
|
if (style.defined.marginRight) definedBits |= 1 << 8;
|
||||||
|
if (style.defined.paddingTop) definedBits |= 1 << 9;
|
||||||
|
if (style.defined.paddingBottom) definedBits |= 1 << 10;
|
||||||
|
if (style.defined.paddingLeft) definedBits |= 1 << 11;
|
||||||
|
if (style.defined.paddingRight) definedBits |= 1 << 12;
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CSS", "Saved %u rules to cache", ruleCount);
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::loadFromCache() {
|
||||||
|
if (cachePath.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing rules
|
||||||
|
clear();
|
||||||
|
|
||||||
|
// Read and verify version
|
||||||
|
uint8_t version = 0;
|
||||||
|
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) {
|
||||||
|
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION);
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read rule count
|
||||||
|
uint16_t ruleCount = 0;
|
||||||
|
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read each rule
|
||||||
|
for (uint16_t i = 0; i < ruleCount; ++i) {
|
||||||
|
// Read selector string
|
||||||
|
uint16_t selectorLen = 0;
|
||||||
|
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string selector;
|
||||||
|
selector.resize(selectorLen);
|
||||||
|
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read CssStyle fields
|
||||||
|
CssStyle style;
|
||||||
|
uint8_t enumVal;
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.textAlign = static_cast<CssTextAlign>(enumVal);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.fontStyle = static_cast<CssFontStyle>(enumVal);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.fontWeight = static_cast<CssFontWeight>(enumVal);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
||||||
|
|
||||||
|
// Read CssLength fields
|
||||||
|
auto readLength = [&file](CssLength& len) -> bool {
|
||||||
|
if (file.read(&len.value, sizeof(len.value)) != sizeof(len.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t unitVal;
|
||||||
|
if (file.read(&unitVal, 1) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
len.unit = static_cast<CssUnit>(unitVal);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
|
||||||
|
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
|
||||||
|
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read defined flags
|
||||||
|
uint16_t definedBits = 0;
|
||||||
|
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.defined.textAlign = (definedBits & 1 << 0) != 0;
|
||||||
|
style.defined.fontStyle = (definedBits & 1 << 1) != 0;
|
||||||
|
style.defined.fontWeight = (definedBits & 1 << 2) != 0;
|
||||||
|
style.defined.textDecoration = (definedBits & 1 << 3) != 0;
|
||||||
|
style.defined.textIndent = (definedBits & 1 << 4) != 0;
|
||||||
|
style.defined.marginTop = (definedBits & 1 << 5) != 0;
|
||||||
|
style.defined.marginBottom = (definedBits & 1 << 6) != 0;
|
||||||
|
style.defined.marginLeft = (definedBits & 1 << 7) != 0;
|
||||||
|
style.defined.marginRight = (definedBits & 1 << 8) != 0;
|
||||||
|
style.defined.paddingTop = (definedBits & 1 << 9) != 0;
|
||||||
|
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
|
||||||
|
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
|
||||||
|
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
|
||||||
|
|
||||||
|
rulesBySelector_[selector] = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
122
lib/Epub/Epub/css/CssParser.h
Normal file
122
lib/Epub/Epub/css/CssParser.h
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CssStyle.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight CSS parser for EPUB stylesheets
|
||||||
|
*
|
||||||
|
* Parses CSS files and extracts styling information relevant for e-ink display.
|
||||||
|
* Uses a two-phase approach: first tokenizes the CSS content, then builds
|
||||||
|
* a rule database that can be queried during HTML parsing.
|
||||||
|
*
|
||||||
|
* Supported selectors:
|
||||||
|
* - Element selectors: p, div, h1, etc.
|
||||||
|
* - Class selectors: .classname
|
||||||
|
* - Combined: element.classname
|
||||||
|
* - Grouped: selector1, selector2 { }
|
||||||
|
*
|
||||||
|
* Not supported (silently ignored):
|
||||||
|
* - Descendant/child selectors
|
||||||
|
* - Pseudo-classes and pseudo-elements
|
||||||
|
* - Media queries (content is skipped)
|
||||||
|
* - @import, @font-face, etc.
|
||||||
|
*/
|
||||||
|
class CssParser {
|
||||||
|
public:
|
||||||
|
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||||
|
~CssParser() = default;
|
||||||
|
|
||||||
|
// Non-copyable
|
||||||
|
CssParser(const CssParser&) = delete;
|
||||||
|
CssParser& operator=(const CssParser&) = delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and parse CSS from a file stream.
|
||||||
|
* Can be called multiple times to accumulate rules from multiple stylesheets.
|
||||||
|
* @param source Open file handle to read from
|
||||||
|
* @return true if parsing completed (even if no rules found)
|
||||||
|
*/
|
||||||
|
bool loadFromStream(FsFile& source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the style for an HTML element, considering tag name and class attributes.
|
||||||
|
* Applies CSS cascade: element style < class style < element.class style
|
||||||
|
*
|
||||||
|
* @param tagName The HTML element name (e.g., "p", "div")
|
||||||
|
* @param classAttr The class attribute value (may contain multiple space-separated classes)
|
||||||
|
* @return Combined style with all applicable rules merged
|
||||||
|
*/
|
||||||
|
[[nodiscard]] CssStyle resolveStyle(const std::string& tagName, const std::string& classAttr) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an inline style attribute string.
|
||||||
|
* @param styleValue The value of a style="" attribute
|
||||||
|
* @return Parsed style properties
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static CssStyle parseInlineStyle(const std::string& styleValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any rules have been loaded
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool empty() const { return rulesBySelector_.empty(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of loaded rule sets
|
||||||
|
*/
|
||||||
|
[[nodiscard]] size_t ruleCount() const { return rulesBySelector_.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all loaded rules
|
||||||
|
*/
|
||||||
|
void clear() { rulesBySelector_.clear(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CSS rules cache file exists
|
||||||
|
*/
|
||||||
|
bool hasCache() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save parsed CSS rules to a cache file.
|
||||||
|
* @return true if cache was written successfully
|
||||||
|
*/
|
||||||
|
bool saveToCache() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load CSS rules from a cache file.
|
||||||
|
* Clears any existing rules before loading.
|
||||||
|
* @return true if cache was loaded successfully
|
||||||
|
*/
|
||||||
|
bool loadFromCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Storage: maps normalized selector -> style properties
|
||||||
|
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
||||||
|
|
||||||
|
std::string cachePath;
|
||||||
|
|
||||||
|
// Internal parsing helpers
|
||||||
|
void processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style);
|
||||||
|
static CssStyle parseDeclarations(const std::string& declBlock);
|
||||||
|
static void parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf,
|
||||||
|
std::string& propValueBuf);
|
||||||
|
|
||||||
|
// Individual property value parsers
|
||||||
|
static CssTextAlign interpretAlignment(const std::string& val);
|
||||||
|
static CssFontStyle interpretFontStyle(const std::string& val);
|
||||||
|
static CssFontWeight interpretFontWeight(const std::string& val);
|
||||||
|
static CssTextDecoration interpretDecoration(const std::string& val);
|
||||||
|
static CssLength interpretLength(const std::string& val);
|
||||||
|
|
||||||
|
// String utilities
|
||||||
|
static std::string normalized(const std::string& s);
|
||||||
|
static void normalizedInto(const std::string& s, std::string& out);
|
||||||
|
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
|
||||||
|
static std::vector<std::string> splitWhitespace(const std::string& s);
|
||||||
|
};
|
||||||
202
lib/Epub/Epub/css/CssStyle.h
Normal file
202
lib/Epub/Epub/css/CssStyle.h
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings
|
||||||
|
enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 };
|
||||||
|
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 };
|
||||||
|
|
||||||
|
// Represents a CSS length value with its unit, allowing deferred resolution to pixels
|
||||||
|
struct CssLength {
|
||||||
|
float value = 0.0f;
|
||||||
|
CssUnit unit = CssUnit::Pixels;
|
||||||
|
|
||||||
|
CssLength() = default;
|
||||||
|
CssLength(const float v, const CssUnit u) : value(v), unit(u) {}
|
||||||
|
|
||||||
|
// Convenience constructor for pixel values (most common case)
|
||||||
|
explicit CssLength(const float pixels) : value(pixels) {}
|
||||||
|
|
||||||
|
// Returns true if this length can be resolved to pixels with the given context.
|
||||||
|
// Percentage units require a non-zero containerWidth to resolve.
|
||||||
|
[[nodiscard]] bool isResolvable(const float containerWidth = 0) const {
|
||||||
|
return unit != CssUnit::Percent || containerWidth > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve to pixels given the current em size (font line height)
|
||||||
|
// containerWidth is needed for percentage units (e.g. viewport width)
|
||||||
|
[[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const {
|
||||||
|
switch (unit) {
|
||||||
|
case CssUnit::Em:
|
||||||
|
case CssUnit::Rem:
|
||||||
|
return value * emSize;
|
||||||
|
case CssUnit::Points:
|
||||||
|
return value * 1.33f; // Approximate pt to px conversion
|
||||||
|
case CssUnit::Percent:
|
||||||
|
return value * containerWidth / 100.0f;
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve to int16_t pixels (for BlockStyle fields)
|
||||||
|
[[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const {
|
||||||
|
return static_cast<int16_t>(toPixels(emSize, containerWidth));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Font style options matching CSS font-style property
|
||||||
|
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
||||||
|
|
||||||
|
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
||||||
|
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||||
|
|
||||||
|
// Text decoration options
|
||||||
|
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
||||||
|
|
||||||
|
// Bitmask for tracking which properties have been explicitly set
|
||||||
|
struct CssPropertyFlags {
|
||||||
|
uint16_t textAlign : 1;
|
||||||
|
uint16_t fontStyle : 1;
|
||||||
|
uint16_t fontWeight : 1;
|
||||||
|
uint16_t textDecoration : 1;
|
||||||
|
uint16_t textIndent : 1;
|
||||||
|
uint16_t marginTop : 1;
|
||||||
|
uint16_t marginBottom : 1;
|
||||||
|
uint16_t marginLeft : 1;
|
||||||
|
uint16_t marginRight : 1;
|
||||||
|
uint16_t paddingTop : 1;
|
||||||
|
uint16_t paddingBottom : 1;
|
||||||
|
uint16_t paddingLeft : 1;
|
||||||
|
uint16_t paddingRight : 1;
|
||||||
|
|
||||||
|
CssPropertyFlags()
|
||||||
|
: textAlign(0),
|
||||||
|
fontStyle(0),
|
||||||
|
fontWeight(0),
|
||||||
|
textDecoration(0),
|
||||||
|
textIndent(0),
|
||||||
|
marginTop(0),
|
||||||
|
marginBottom(0),
|
||||||
|
marginLeft(0),
|
||||||
|
marginRight(0),
|
||||||
|
paddingTop(0),
|
||||||
|
paddingBottom(0),
|
||||||
|
paddingLeft(0),
|
||||||
|
paddingRight(0) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool anySet() const {
|
||||||
|
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||||||
|
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAll() {
|
||||||
|
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||||
|
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||||
|
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Represents a collection of CSS style properties
|
||||||
|
// Only stores properties relevant to e-ink text rendering
|
||||||
|
// Length values are stored as CssLength (value + unit) for deferred resolution
|
||||||
|
struct CssStyle {
|
||||||
|
CssTextAlign textAlign = CssTextAlign::Left;
|
||||||
|
CssFontStyle fontStyle = CssFontStyle::Normal;
|
||||||
|
CssFontWeight fontWeight = CssFontWeight::Normal;
|
||||||
|
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||||
|
|
||||||
|
CssLength textIndent; // First-line indent (deferred resolution)
|
||||||
|
CssLength marginTop; // Vertical spacing before block
|
||||||
|
CssLength marginBottom; // Vertical spacing after block
|
||||||
|
CssLength marginLeft; // Horizontal spacing left of block
|
||||||
|
CssLength marginRight; // Horizontal spacing right of block
|
||||||
|
CssLength paddingTop; // Padding before
|
||||||
|
CssLength paddingBottom; // Padding after
|
||||||
|
CssLength paddingLeft; // Padding left
|
||||||
|
CssLength paddingRight; // Padding right
|
||||||
|
|
||||||
|
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||||
|
|
||||||
|
// Apply properties from another style, only overwriting if the other style
|
||||||
|
// has that property explicitly defined
|
||||||
|
void applyOver(const CssStyle& base) {
|
||||||
|
if (base.hasTextAlign()) {
|
||||||
|
textAlign = base.textAlign;
|
||||||
|
defined.textAlign = 1;
|
||||||
|
}
|
||||||
|
if (base.hasFontStyle()) {
|
||||||
|
fontStyle = base.fontStyle;
|
||||||
|
defined.fontStyle = 1;
|
||||||
|
}
|
||||||
|
if (base.hasFontWeight()) {
|
||||||
|
fontWeight = base.fontWeight;
|
||||||
|
defined.fontWeight = 1;
|
||||||
|
}
|
||||||
|
if (base.hasTextDecoration()) {
|
||||||
|
textDecoration = base.textDecoration;
|
||||||
|
defined.textDecoration = 1;
|
||||||
|
}
|
||||||
|
if (base.hasTextIndent()) {
|
||||||
|
textIndent = base.textIndent;
|
||||||
|
defined.textIndent = 1;
|
||||||
|
}
|
||||||
|
if (base.hasMarginTop()) {
|
||||||
|
marginTop = base.marginTop;
|
||||||
|
defined.marginTop = 1;
|
||||||
|
}
|
||||||
|
if (base.hasMarginBottom()) {
|
||||||
|
marginBottom = base.marginBottom;
|
||||||
|
defined.marginBottom = 1;
|
||||||
|
}
|
||||||
|
if (base.hasMarginLeft()) {
|
||||||
|
marginLeft = base.marginLeft;
|
||||||
|
defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
if (base.hasMarginRight()) {
|
||||||
|
marginRight = base.marginRight;
|
||||||
|
defined.marginRight = 1;
|
||||||
|
}
|
||||||
|
if (base.hasPaddingTop()) {
|
||||||
|
paddingTop = base.paddingTop;
|
||||||
|
defined.paddingTop = 1;
|
||||||
|
}
|
||||||
|
if (base.hasPaddingBottom()) {
|
||||||
|
paddingBottom = base.paddingBottom;
|
||||||
|
defined.paddingBottom = 1;
|
||||||
|
}
|
||||||
|
if (base.hasPaddingLeft()) {
|
||||||
|
paddingLeft = base.paddingLeft;
|
||||||
|
defined.paddingLeft = 1;
|
||||||
|
}
|
||||||
|
if (base.hasPaddingRight()) {
|
||||||
|
paddingRight = base.paddingRight;
|
||||||
|
defined.paddingRight = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
||||||
|
[[nodiscard]] bool hasFontStyle() const { return defined.fontStyle; }
|
||||||
|
[[nodiscard]] bool hasFontWeight() const { return defined.fontWeight; }
|
||||||
|
[[nodiscard]] bool hasTextDecoration() const { return defined.textDecoration; }
|
||||||
|
[[nodiscard]] bool hasTextIndent() const { return defined.textIndent; }
|
||||||
|
[[nodiscard]] bool hasMarginTop() const { return defined.marginTop; }
|
||||||
|
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||||||
|
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||||||
|
[[nodiscard]] bool hasMarginRight() const { return defined.marginRight; }
|
||||||
|
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||||||
|
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||||
|
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||||||
|
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
textAlign = CssTextAlign::Left;
|
||||||
|
fontStyle = CssFontStyle::Normal;
|
||||||
|
fontWeight = CssFontWeight::Normal;
|
||||||
|
textDecoration = CssTextDecoration::None;
|
||||||
|
textIndent = CssLength{};
|
||||||
|
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||||
|
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||||
|
defined.clearAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
76
lib/Epub/Epub/htmlEntities.cpp
Normal file
76
lib/Epub/Epub/htmlEntities.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// from
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#include "htmlEntities.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
struct EntityPair {
|
||||||
|
const char* key;
|
||||||
|
const char* value;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const EntityPair ENTITY_LOOKUP[] = {
|
||||||
|
{""", "\""}, {"⁄", "⁄"}, {"&", "&"}, {"<", "<"}, {">", ">"},
|
||||||
|
{"À", "À"}, {"Á", "Á"}, {"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"},
|
||||||
|
{"Å", "Å"}, {"Æ", "Æ"}, {"Ç", "Ç"}, {"È", "È"}, {"É", "É"},
|
||||||
|
{"Ê", "Ê"}, {"Ë", "Ë"}, {"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"},
|
||||||
|
{"Ï", "Ï"}, {"Ð", "Ð"}, {"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"},
|
||||||
|
{"Ô", "Ô"}, {"Õ", "Õ"}, {"Ö", "Ö"}, {"Ø", "Ø"}, {"Ù", "Ù"},
|
||||||
|
{"Ú", "Ú"}, {"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"},
|
||||||
|
{"ß", "ß"}, {"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"},
|
||||||
|
{"ä", "ä"}, {"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"},
|
||||||
|
{"é", "é"}, {"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"},
|
||||||
|
{"î", "î"}, {"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"},
|
||||||
|
{"ó", "ó"}, {"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"ø", "ø"},
|
||||||
|
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"},
|
||||||
|
{"þ", "þ"}, {"ÿ", "ÿ"}, {" ", "\xC2\xA0"}, {"¡", "¡"}, {"¢", "¢"},
|
||||||
|
{"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, {"§", "§"},
|
||||||
|
{"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, {"¬", "¬"},
|
||||||
|
{"­", ""}, {"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"},
|
||||||
|
{"²", "²"}, {"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"},
|
||||||
|
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"},
|
||||||
|
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"×", "×"}, {"÷", "÷"},
|
||||||
|
{"∀", "∀"}, {"∂", "∂"}, {"∃", "∃"}, {"∅", "∅"}, {"∇", "∇"},
|
||||||
|
{"∈", "∈"}, {"∉", "∉"}, {"∋", "∋"}, {"∏", "∏"}, {"∑", "∑"},
|
||||||
|
{"−", "−"}, {"∗", "∗"}, {"√", "√"}, {"∝", "∝"}, {"∞", "∞"},
|
||||||
|
{"∠", "∠"}, {"∧", "∧"}, {"∨", "∨"}, {"∩", "∩"}, {"∪", "∪"},
|
||||||
|
{"∫", "∫"}, {"∴", "∴"}, {"∼", "∼"}, {"≅", "≅"}, {"≈", "≈"},
|
||||||
|
{"≠", "≠"}, {"≡", "≡"}, {"≤", "≤"}, {"≥", "≥"}, {"⊂", "⊂"},
|
||||||
|
{"⊃", "⊃"}, {"⊄", "⊄"}, {"⊆", "⊆"}, {"⊇", "⊇"}, {"⊕", "⊕"},
|
||||||
|
{"⊗", "⊗"}, {"⊥", "⊥"}, {"⋅", "⋅"}, {"Α", "Α"}, {"Β", "Β"},
|
||||||
|
{"Γ", "Γ"}, {"Δ", "Δ"}, {"Ε", "Ε"}, {"Ζ", "Ζ"}, {"Η", "Η"},
|
||||||
|
{"Θ", "Θ"}, {"Ι", "Ι"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"},
|
||||||
|
{"Ν", "Ν"}, {"Ξ", "Ξ"}, {"Ο", "Ο"}, {"Π", "Π"}, {"Ρ", "Ρ"},
|
||||||
|
{"Σ", "Σ"}, {"Τ", "Τ"}, {"Υ", "Υ"}, {"Φ", "Φ"}, {"Χ", "Χ"},
|
||||||
|
{"Ψ", "Ψ"}, {"Ω", "Ω"}, {"α", "α"}, {"β", "β"}, {"γ", "γ"},
|
||||||
|
{"δ", "δ"}, {"ε", "ε"}, {"ζ", "ζ"}, {"η", "η"}, {"θ", "θ"},
|
||||||
|
{"ι", "ι"}, {"κ", "κ"}, {"λ", "λ"}, {"μ", "μ"}, {"ν", "ν"},
|
||||||
|
{"ξ", "ξ"}, {"ο", "ο"}, {"π", "π"}, {"ρ", "ρ"}, {"ς", "ς"},
|
||||||
|
{"σ", "σ"}, {"τ", "τ"}, {"υ", "υ"}, {"φ", "φ"}, {"χ", "χ"},
|
||||||
|
{"ψ", "ψ"}, {"ω", "ω"}, {"ϑ", "ϑ"}, {"ϒ", "ϒ"}, {"ϖ", "ϖ"},
|
||||||
|
{"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, {"š", "š"}, {"Ÿ", "Ÿ"},
|
||||||
|
{"ƒ", "ƒ"}, {"ˆ", "ˆ"}, {"˜", "˜"}, {" ", " "}, {" ", " "},
|
||||||
|
{" ", " "}, {"‌", ""}, {"‍", ""}, {"‎", ""}, {"‏", ""},
|
||||||
|
{"–", "–"}, {"—", "—"}, {"‘", "‘"}, {"’", "’"}, {"‚", "‚"},
|
||||||
|
{"“", "“"}, {"”", "”"}, {"„", "„"}, {"†", "†"}, {"‡", "‡"},
|
||||||
|
{"•", "•"}, {"…", "…"}, {"‰", "‰"}, {"′", "′"}, {"″", "″"},
|
||||||
|
{"‹", "‹"}, {"›", "›"}, {"‾", "‾"}, {"€", "€"}, {"™", "™"},
|
||||||
|
{"←", "←"}, {"↑", "↑"}, {"→", "→"}, {"↓", "↓"}, {"↔", "↔"},
|
||||||
|
{"↵", "↵"}, {"⌈", "⌈"}, {"⌉", "⌉"}, {"⌊", "⌊"}, {"⌋", "⌋"},
|
||||||
|
{"◊", "◊"}, {"♠", "♠"}, {"♣", "♣"}, {"♥", "♥"}, {"♦", "♦"}};
|
||||||
|
|
||||||
|
static const size_t ENTITY_LOOKUP_COUNT = sizeof(ENTITY_LOOKUP) / sizeof(ENTITY_LOOKUP[0]);
|
||||||
|
|
||||||
|
// Lookup a single HTML entity and return its UTF-8 value
|
||||||
|
const char* lookupHtmlEntity(const char* entity, int len) {
|
||||||
|
for (size_t i = 0; i < ENTITY_LOOKUP_COUNT; i++) {
|
||||||
|
const char* key = ENTITY_LOOKUP[i].key;
|
||||||
|
const size_t keyLen = strlen(key);
|
||||||
|
if (static_cast<size_t>(len) == keyLen && memcmp(entity, key, keyLen) == 0) {
|
||||||
|
return ENTITY_LOOKUP[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr; // Entity not found
|
||||||
|
}
|
||||||
9
lib/Epub/Epub/htmlEntities.h
Normal file
9
lib/Epub/Epub/htmlEntities.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// from
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Lookup a single HTML entity (including & and ;) and return its UTF-8 value
|
||||||
|
// Returns nullptr if entity is not found
|
||||||
|
const char* lookupHtmlEntity(const char* entity, int len);
|
||||||
@@ -8,25 +8,28 @@
|
|||||||
#include "generated/hyph-en.trie.h"
|
#include "generated/hyph-en.trie.h"
|
||||||
#include "generated/hyph-es.trie.h"
|
#include "generated/hyph-es.trie.h"
|
||||||
#include "generated/hyph-fr.trie.h"
|
#include "generated/hyph-fr.trie.h"
|
||||||
|
#include "generated/hyph-it.trie.h"
|
||||||
#include "generated/hyph-ru.trie.h"
|
#include "generated/hyph-ru.trie.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||||
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
using EntryArray = std::array<LanguageEntry, 5>;
|
using EntryArray = std::array<LanguageEntry, 6>;
|
||||||
|
|
||||||
const EntryArray& entries() {
|
const EntryArray& entries() {
|
||||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||||
{"french", "fr", &frenchHyphenator},
|
{"french", "fr", &frenchHyphenator},
|
||||||
{"german", "de", &germanHyphenator},
|
{"german", "de", &germanHyphenator},
|
||||||
{"russian", "ru", &russianHyphenator},
|
{"russian", "ru", &russianHyphenator},
|
||||||
{"spanish", "es", &spanishHyphenator}}};
|
{"spanish", "es", &spanishHyphenator},
|
||||||
|
{"italian", "it", &italianHyphenator}}};
|
||||||
return kEntries;
|
return kEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,8 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using EmbeddedAutomaton = SerializedHyphenationPatterns;
|
||||||
|
|
||||||
struct AugmentedWord {
|
struct AugmentedWord {
|
||||||
std::vector<uint8_t> bytes;
|
std::vector<uint8_t> bytes;
|
||||||
std::vector<size_t> charByteOffsets;
|
std::vector<size_t> charByteOffsets;
|
||||||
@@ -141,59 +143,10 @@ struct AutomatonState {
|
|||||||
bool valid() const { return data != nullptr; }
|
bool valid() const { return data != nullptr; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lightweight descriptor for the entire embedded automaton.
|
|
||||||
// The blob format is:
|
|
||||||
// [0..3] - big-endian root offset
|
|
||||||
// [4....] - node heap containing variable-sized headers + transition data
|
|
||||||
struct EmbeddedAutomaton {
|
|
||||||
const uint8_t* data = nullptr;
|
|
||||||
size_t size = 0;
|
|
||||||
uint32_t rootOffset = 0;
|
|
||||||
|
|
||||||
bool valid() const { return data != nullptr && size >= 4 && rootOffset < size; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Decode the serialized automaton header and root offset.
|
|
||||||
EmbeddedAutomaton parseAutomaton(const SerializedHyphenationPatterns& patterns) {
|
|
||||||
EmbeddedAutomaton automaton;
|
|
||||||
if (!patterns.data || patterns.size < 4) {
|
|
||||||
return automaton;
|
|
||||||
}
|
|
||||||
|
|
||||||
automaton.data = patterns.data;
|
|
||||||
automaton.size = patterns.size;
|
|
||||||
automaton.rootOffset = (static_cast<uint32_t>(patterns.data[0]) << 24) |
|
|
||||||
(static_cast<uint32_t>(patterns.data[1]) << 16) |
|
|
||||||
(static_cast<uint32_t>(patterns.data[2]) << 8) | static_cast<uint32_t>(patterns.data[3]);
|
|
||||||
if (automaton.rootOffset >= automaton.size) {
|
|
||||||
automaton.data = nullptr;
|
|
||||||
automaton.size = 0;
|
|
||||||
}
|
|
||||||
return automaton;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache parsed automata per blob pointer to avoid reparsing.
|
|
||||||
const EmbeddedAutomaton& getAutomaton(const SerializedHyphenationPatterns& patterns) {
|
|
||||||
struct CacheEntry {
|
|
||||||
const SerializedHyphenationPatterns* key;
|
|
||||||
EmbeddedAutomaton automaton;
|
|
||||||
};
|
|
||||||
static std::vector<CacheEntry> cache;
|
|
||||||
|
|
||||||
for (const auto& entry : cache) {
|
|
||||||
if (entry.key == &patterns) {
|
|
||||||
return entry.automaton;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.push_back({&patterns, parseAutomaton(patterns)});
|
|
||||||
return cache.back().automaton;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interpret the node located at `addr`, returning transition metadata.
|
// Interpret the node located at `addr`, returning transition metadata.
|
||||||
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
||||||
AutomatonState state;
|
AutomatonState state;
|
||||||
if (!automaton.valid() || addr >= automaton.size) {
|
if (addr >= automaton.size) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +187,7 @@ AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
|||||||
if (offset + levelsLen > automaton.size) {
|
if (offset + levelsLen > automaton.size) {
|
||||||
return AutomatonState{};
|
return AutomatonState{};
|
||||||
}
|
}
|
||||||
levelsPtr = automaton.data + offset;
|
levelsPtr = automaton.data + offset - 4u;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pos + childCount > remaining) {
|
if (pos + childCount > remaining) {
|
||||||
@@ -344,10 +297,7 @@ std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmbeddedAutomaton& automaton = getAutomaton(patterns);
|
const EmbeddedAutomaton& automaton = patterns;
|
||||||
if (!automaton.valid()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const AutomatonState root = decodeState(automaton, automaton.rootOffset);
|
const AutomatonState root = decodeState(automaton, automaton.rootOffset);
|
||||||
if (!root.valid()) {
|
if (!root.valid()) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
|
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
|
||||||
struct SerializedHyphenationPatterns {
|
struct SerializedHyphenationPatterns {
|
||||||
|
size_t rootOffset;
|
||||||
const std::uint8_t* data;
|
const std::uint8_t* data;
|
||||||
size_t size;
|
size_t size;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,377 +7,447 @@
|
|||||||
|
|
||||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
alignas(4) constexpr uint8_t fr_trie_data[] = {
|
alignas(4) constexpr uint8_t fr_trie_data[] = {
|
||||||
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C,
|
0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x2B,
|
||||||
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F,
|
0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17,
|
||||||
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C,
|
0x04, 0x1F, 0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36,
|
||||||
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D,
|
0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C,
|
||||||
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E,
|
0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B,
|
||||||
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16,
|
0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x48, 0x2C, 0x0B, 0x29, 0x16,
|
||||||
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16,
|
0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x3E, 0x0D,
|
||||||
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B,
|
||||||
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C,
|
0x16, 0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD,
|
||||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD,
|
0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||||
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63,
|
||||||
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD,
|
||||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD,
|
0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0,
|
||||||
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD,
|
0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
||||||
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74,
|
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD,
|
||||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF,
|
0x21, 0x6E, 0xFD, 0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61,
|
||||||
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21,
|
0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA,
|
||||||
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72,
|
0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD,
|
||||||
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD,
|
0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x5E, 0x21, 0x74, 0xFC,
|
||||||
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x74,
|
||||||
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61,
|
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70,
|
||||||
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74,
|
0x73, 0x72, 0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF,
|
||||||
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74,
|
0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB,
|
||||||
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75,
|
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0,
|
||||||
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF,
|
0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0x6E, 0x75, 0xF2, 0xFD, 0x21,
|
||||||
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD,
|
0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0xFF, 0x06,
|
||||||
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21,
|
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21,
|
||||||
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
0x74, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2,
|
||||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41,
|
0x02, 0x52, 0x6E, 0x75, 0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD,
|
||||||
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62,
|
0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70,
|
||||||
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06,
|
0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD,
|
||||||
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21,
|
0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x72, 0xFD, 0x21,
|
||||||
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
||||||
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF,
|
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96,
|
||||||
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65,
|
0xFF, 0x96, 0x41, 0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52,
|
||||||
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02,
|
0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05,
|
||||||
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5,
|
0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75,
|
||||||
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8,
|
0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0xC2, 0x02,
|
||||||
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64,
|
0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74,
|
||||||
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67,
|
0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF,
|
||||||
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01,
|
0xF4, 0xFF, 0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7,
|
||||||
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1,
|
0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD,
|
||||||
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE,
|
0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||||
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xFD, 0xA0, 0x00, 0x61,
|
||||||
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75,
|
0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0xFE, 0xC8,
|
||||||
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC,
|
0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43,
|
||||||
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21,
|
0x64, 0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E,
|
||||||
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72,
|
0x70, 0x73, 0x72, 0x67, 0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D,
|
||||||
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21,
|
0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||||
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04,
|
0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2,
|
||||||
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD,
|
0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x47, 0xFE, 0x47,
|
||||||
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41,
|
0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
||||||
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43,
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04,
|
0x63, 0x74, 0x75, 0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F,
|
||||||
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61,
|
0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0,
|
||||||
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0,
|
0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75,
|
||||||
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04,
|
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||||
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3,
|
0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0x6C,
|
||||||
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73,
|
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD,
|
||||||
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
0xA0, 0x04, 0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21,
|
||||||
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF,
|
0xC3, 0xFD, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD,
|
||||||
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41,
|
0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02,
|
||||||
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72,
|
0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05,
|
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x31, 0x21,
|
||||||
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E,
|
0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41,
|
||||||
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44,
|
0x61, 0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41,
|
||||||
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86,
|
0x6F, 0xFE, 0x7B, 0xA0, 0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21,
|
||||||
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D,
|
0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63,
|
||||||
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41,
|
0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3,
|
||||||
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A,
|
0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x74,
|
||||||
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F,
|
0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
||||||
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75,
|
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E,
|
||||||
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61,
|
0x72, 0x73, 0xFF, 0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9,
|
||||||
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63,
|
0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C,
|
||||||
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21,
|
0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1,
|
||||||
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x23, 0x21, 0x6E, 0xFD,
|
||||||
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21,
|
0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0xFF,
|
||||||
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6,
|
0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF,
|
||||||
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F,
|
0xFD, 0x44, 0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9,
|
||||||
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68,
|
0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64,
|
||||||
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF,
|
0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C,
|
||||||
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74,
|
0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x68, 0xFC, 0x92, 0x23, 0x61,
|
||||||
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01,
|
0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0xFC, 0x5A,
|
||||||
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA,
|
0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79,
|
||||||
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73,
|
0x6F, 0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69,
|
||||||
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E,
|
0x6F, 0x73, 0x74, 0x75, 0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0,
|
||||||
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21,
|
0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE,
|
||||||
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21,
|
0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63,
|
||||||
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75,
|
0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x6E, 0xFB, 0x41,
|
||||||
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61,
|
0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
||||||
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA,
|
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21,
|
||||||
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
0xC3, 0xFC, 0x21, 0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3,
|
||||||
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21,
|
0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64,
|
||||||
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF,
|
0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD,
|
||||||
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43,
|
0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xFE, 0xCA, 0x21, 0x6F,
|
||||||
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21,
|
0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0x44,
|
||||||
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB,
|
0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5,
|
||||||
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE,
|
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD,
|
||||||
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF,
|
0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21,
|
||||||
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74,
|
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA,
|
||||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41,
|
0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xFA, 0xA4, 0xFA, 0xA4, 0xFF,
|
||||||
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC,
|
0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0xFD, 0x44,
|
||||||
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25,
|
0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5,
|
||||||
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC,
|
0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41,
|
||||||
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC,
|
0xA9, 0xFC, 0x27, 0x21, 0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21,
|
||||||
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70,
|
0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2,
|
||||||
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC,
|
0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73,
|
||||||
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0x21, 0xA9,
|
||||||
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41,
|
0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
||||||
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08,
|
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4,
|
||||||
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9,
|
0xFC, 0xBD, 0x21, 0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93,
|
||||||
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF,
|
0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9,
|
||||||
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75,
|
0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD,
|
||||||
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD,
|
0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xA0, 0x01, 0xC1, 0x21,
|
||||||
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18,
|
0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x81,
|
||||||
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2,
|
0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41,
|
||||||
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65,
|
0x73, 0xFE, 0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD,
|
||||||
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF,
|
0x43, 0x6F, 0x73, 0x75, 0xFF, 0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD,
|
||||||
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63,
|
0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69,
|
0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0x61, 0xFE, 0xA9, 0x21, 0x69,
|
||||||
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21,
|
0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0x21, 0x74,
|
||||||
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21,
|
0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD,
|
||||||
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7,
|
0x25, 0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB,
|
||||||
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
0xFD, 0x21, 0x61, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD,
|
||||||
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2,
|
0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68,
|
||||||
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47,
|
0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA,
|
||||||
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7,
|
0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xFB,
|
||||||
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9,
|
0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
||||||
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63,
|
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7,
|
||||||
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8,
|
0xFF, 0xFD, 0x41, 0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C,
|
||||||
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75,
|
0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80,
|
||||||
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6,
|
0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9,
|
||||||
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6,
|
0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0xD7, 0xFF, 0xE4, 0xFD,
|
||||||
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD,
|
0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xB9,
|
||||||
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61,
|
0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21,
|
||||||
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6,
|
0xA9, 0xFD, 0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9,
|
||||||
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8,
|
0x42, 0x66, 0x78, 0xFB, 0x18, 0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1,
|
||||||
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61,
|
0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41,
|
||||||
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8,
|
0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x63, 0x68, 0x75, 0x6F, 0xFF,
|
||||||
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0xFD, 0xC3,
|
||||||
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0,
|
0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43,
|
||||||
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
|
0x63, 0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F,
|
||||||
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2,
|
0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41,
|
||||||
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21,
|
0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||||
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63,
|
0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0xF8, 0xFF, 0xF9,
|
||||||
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
||||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41,
|
||||||
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1,
|
0x69, 0xF7, 0xD2, 0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73,
|
||||||
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70,
|
0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF,
|
||||||
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70,
|
||||||
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21,
|
0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x41, 0x75, 0xF8, 0x30,
|
||||||
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21,
|
0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0xF8,
|
||||||
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03,
|
0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41,
|
||||||
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
|
0x73, 0xF8, 0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41,
|
||||||
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
0x69, 0xF8, 0x73, 0x21, 0x75, 0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA,
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72,
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD,
|
0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0xBF, 0xF6, 0xBF, 0x42, 0x63,
|
||||||
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76,
|
0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x74,
|
||||||
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2,
|
0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2,
|
||||||
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
0x61, 0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF,
|
||||||
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21,
|
0xF9, 0xF6, 0x99, 0xF6, 0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF,
|
||||||
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4,
|
0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF,
|
||||||
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD,
|
0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xC3, 0x62, 0x63, 0x64, 0x65, 0x69,
|
||||||
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2,
|
0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0xB1, 0xF8, 0xE6,
|
||||||
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE,
|
0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
||||||
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65,
|
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85,
|
||||||
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21,
|
0xF8, 0x85, 0xA0, 0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21,
|
||||||
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF,
|
0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73,
|
||||||
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21,
|
||||||
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF,
|
0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F,
|
||||||
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x61,
|
||||||
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
||||||
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92,
|
0xFD, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74,
|
||||||
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF,
|
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74,
|
||||||
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F,
|
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD,
|
||||||
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD,
|
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0xE1, 0xFD, 0x41, 0x74, 0xFE,
|
||||||
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41,
|
0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xF6, 0xFD,
|
||||||
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD,
|
0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD,
|
||||||
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21,
|
0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41,
|
||||||
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA,
|
0x2E, 0xFF, 0x33, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63,
|
||||||
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21,
|
0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22,
|
||||||
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2,
|
0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||||
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
|
||||||
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59,
|
0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
||||||
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE,
|
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E,
|
||||||
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00,
|
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD,
|
||||||
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64,
|
||||||
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0,
|
0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6,
|
||||||
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75,
|
0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xC9, 0xFF, 0xD4,
|
||||||
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7,
|
0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||||
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21,
|
0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02,
|
||||||
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
0x41, 0x21, 0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1,
|
||||||
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69,
|
0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2,
|
||||||
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69,
|
0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74,
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9,
|
0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x00, 0xE2, 0x65, 0x75, 0xFF,
|
||||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C,
|
0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0x62, 0xFF,
|
||||||
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2,
|
0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43,
|
||||||
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC,
|
0x65, 0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92,
|
||||||
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD,
|
0x21, 0x65, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65,
|
||||||
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3,
|
0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
||||||
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41,
|
0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD,
|
||||||
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74,
|
0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0xE7, 0xFF, 0xF6,
|
||||||
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
||||||
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9,
|
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21,
|
0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21,
|
||||||
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41,
|
0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC,
|
||||||
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48,
|
||||||
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73,
|
0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFF, 0xA8, 0xFF, 0xBF,
|
||||||
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34,
|
0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x8D,
|
||||||
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E,
|
0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75,
|
||||||
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A,
|
0xFD, 0x41, 0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21,
|
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70,
|
||||||
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9,
|
0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||||
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA,
|
0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0xFF, 0xFD, 0x42, 0x2E, 0x73,
|
||||||
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42,
|
0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x74, 0xFD,
|
||||||
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB,
|
0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F,
|
||||||
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1,
|
0xE2, 0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21,
|
||||||
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41,
|
0x6E, 0xFC, 0x21, 0x65, 0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70,
|
||||||
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE,
|
||||||
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63,
|
0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41,
|
||||||
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD,
|
0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xE2, 0x2E, 0x62,
|
||||||
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
||||||
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D,
|
||||||
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3,
|
0xFC, 0xEE, 0xA0, 0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5,
|
||||||
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21,
|
0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72,
|
||||||
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65,
|
0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD,
|
||||||
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62,
|
0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0x72, 0xFC, 0x41, 0x69,
|
||||||
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD,
|
0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||||
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01,
|
0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4,
|
||||||
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
0x21, 0x69, 0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF,
|
||||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9,
|
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC,
|
||||||
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1,
|
0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7,
|
||||||
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21,
|
0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF,
|
||||||
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75,
|
0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0x88, 0xA1,
|
||||||
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69,
|
||||||
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0,
|
0xFC, 0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE,
|
||||||
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47,
|
0x8A, 0xFD, 0x27, 0xFD, 0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF,
|
||||||
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00,
|
0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1,
|
||||||
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0,
|
0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC,
|
||||||
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69,
|
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0xF2, 0x5A, 0xA1,
|
||||||
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA,
|
0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
||||||
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22,
|
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2,
|
||||||
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22,
|
0xF5, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0,
|
||||||
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73,
|
0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65,
|
||||||
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF,
|
0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1,
|
||||||
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1,
|
0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFB, 0x41, 0xFF, 0xFB,
|
||||||
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F,
|
0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0xFC,
|
||||||
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C,
|
0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73,
|
||||||
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F,
|
0xFB, 0x34, 0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69,
|
||||||
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66,
|
0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08,
|
||||||
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54,
|
||||||
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63,
|
0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xF3,
|
||||||
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD,
|
0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0xFF, 0xFC,
|
||||||
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F,
|
0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5,
|
||||||
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21,
|
0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF,
|
||||||
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72,
|
0xFC, 0xFB, 0x62, 0x42, 0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E,
|
||||||
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61,
|
0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C,
|
||||||
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF,
|
0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD,
|
||||||
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68,
|
0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x68, 0xF8, 0xC0,
|
||||||
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21,
|
0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
||||||
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61,
|
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A,
|
||||||
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61,
|
0xC3, 0x69, 0x63, 0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39,
|
||||||
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB,
|
0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03,
|
||||||
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21,
|
0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A,
|
||||||
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x76,
|
||||||
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1,
|
0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0x73,
|
||||||
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D,
|
||||||
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2,
|
||||||
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C,
|
0x69, 0x75, 0xC3, 0x6F, 0x65, 0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41,
|
||||||
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F,
|
0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9,
|
||||||
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F,
|
0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFC, 0x3E, 0xFC, 0x3E, 0x41,
|
||||||
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF,
|
0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0xFF, 0xFC,
|
||||||
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6,
|
0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21,
|
||||||
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45,
|
0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C,
|
||||||
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21,
|
0xEB, 0xFD, 0x42, 0xA9, 0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||||
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79,
|
0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D,
|
||||||
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D,
|
0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21,
|
||||||
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D,
|
0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0x65, 0xF9, 0x7E,
|
||||||
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
||||||
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8,
|
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21,
|
||||||
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7,
|
0x75, 0xFC, 0xA0, 0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00,
|
||||||
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F,
|
0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF,
|
||||||
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB,
|
||||||
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72,
|
0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x07, 0x62, 0x21, 0xA9,
|
||||||
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB,
|
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0xFA,
|
||||||
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0,
|
0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35,
|
||||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5,
|
0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6,
|
||||||
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA,
|
0x41, 0x75, 0xF8, 0xC2, 0x22, 0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC,
|
||||||
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D,
|
0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D,
|
||||||
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1,
|
0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x74, 0x79, 0xFE, 0xAE, 0xFE,
|
||||||
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D,
|
0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xC2, 0xFF,
|
||||||
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5,
|
0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64,
|
||||||
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2,
|
0xF1, 0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC,
|
||||||
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4,
|
0x41, 0x6C, 0xF1, 0x8F, 0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21,
|
||||||
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45,
|
0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42,
|
||||||
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87,
|
0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD,
|
||||||
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2,
|
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0xB5, 0xBC, 0xCE,
|
||||||
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C,
|
0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
||||||
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61,
|
||||||
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69,
|
0xFC, 0x22, 0x63, 0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD,
|
||||||
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1,
|
||||||
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF,
|
0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70,
|
||||||
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02,
|
0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x72, 0xF7, 0x21, 0x68,
|
||||||
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72,
|
0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFA,
|
||||||
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75,
|
0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD,
|
||||||
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21,
|
0x41, 0x61, 0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72,
|
||||||
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65,
|
0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC,
|
||||||
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76,
|
0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69,
|
||||||
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9,
|
0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||||
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66,
|
0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xFF, 0xA5,
|
||||||
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E,
|
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21,
|
||||||
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64,
|
0x61, 0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA,
|
||||||
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21,
|
0xFF, 0xDF, 0xFF, 0xEB, 0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61,
|
||||||
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72,
|
0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63,
|
||||||
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6,
|
||||||
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21,
|
0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0x41, 0x70, 0xED,
|
||||||
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65,
|
0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
||||||
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF,
|
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||||
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11,
|
0x6D, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70,
|
||||||
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21,
|
0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD,
|
||||||
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6,
|
0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21,
|
||||||
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF,
|
0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79,
|
||||||
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82,
|
0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xFD,
|
||||||
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F,
|
0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72,
|
||||||
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74,
|
0xF6, 0xA6, 0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74,
|
||||||
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
0xF0, 0x03, 0xFF, 0xFC, 0x45, 0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD,
|
||||||
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74,
|
0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61,
|
||||||
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73,
|
0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0xFB, 0x9D, 0x21, 0x68, 0xFC,
|
||||||
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3,
|
0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xFB, 0xEE,
|
||||||
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41,
|
||||||
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
0x6D, 0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1,
|
||||||
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D,
|
0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2,
|
||||||
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1,
|
0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21,
|
||||||
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2,
|
0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7,
|
||||||
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65,
|
0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xFF, 0xF3, 0xF7,
|
||||||
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD,
|
0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
||||||
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1,
|
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00,
|
||||||
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76,
|
0xE1, 0x6D, 0x72, 0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65,
|
||||||
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21,
|
0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3,
|
||||||
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42,
|
0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5,
|
||||||
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2,
|
0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0x41,
|
||||||
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74,
|
0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x2E,
|
||||||
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42,
|
0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20,
|
||||||
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41,
|
0xFF, 0x4D, 0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC,
|
||||||
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2,
|
0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0,
|
0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75,
|
||||||
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0x32, 0xFF, 0xFD, 0xC2, 0x00,
|
||||||
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0,
|
0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0x65, 0x69,
|
||||||
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8,
|
0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD,
|
||||||
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0,
|
0xC4, 0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD,
|
||||||
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A,
|
0xC4, 0xF4, 0xD1, 0x45, 0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4,
|
||||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79,
|
0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD,
|
||||||
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80,
|
0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2,
|
||||||
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF,
|
0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0x61, 0xC3, 0x65,
|
||||||
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0,
|
0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
||||||
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1,
|
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4,
|
||||||
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E,
|
0x79, 0x41, 0x69, 0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5,
|
||||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F,
|
0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5,
|
||||||
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF,
|
0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21,
|
||||||
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38,
|
0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0x6F, 0x73, 0xF5, 0x12,
|
||||||
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2,
|
0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0x70,
|
||||||
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF,
|
0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00,
|
||||||
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
0xE2, 0x75, 0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD,
|
||||||
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0,
|
0x41, 0x6D, 0xF4, 0x02, 0x21, 0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22,
|
||||||
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98,
|
0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0,
|
||||||
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE,
|
0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0xFF, 0xFC, 0xF3, 0xDA, 0x41,
|
||||||
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xFB, 0x17,
|
||||||
|
0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6,
|
||||||
|
0x66, 0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29,
|
||||||
|
0xC6, 0x00, 0xE2, 0x2E, 0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF,
|
||||||
|
0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||||
|
0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21,
|
||||||
|
0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA9,
|
||||||
|
0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
||||||
|
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65,
|
||||||
|
0xF3, 0x44, 0x21, 0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9,
|
||||||
|
0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5,
|
||||||
|
0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F,
|
||||||
|
0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x41, 0x69, 0xF3, 0x97,
|
||||||
|
0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0x6E,
|
||||||
|
0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41,
|
||||||
|
0x6F, 0xF6, 0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62,
|
||||||
|
0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F,
|
||||||
|
0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21,
|
||||||
|
0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF7, 0x81, 0x21, 0x72, 0xFC,
|
||||||
|
0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF7, 0x6F,
|
||||||
|
0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||||
|
0x63, 0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC,
|
||||||
|
0xDC, 0xFB, 0x21, 0x74, 0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D,
|
||||||
|
0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D,
|
||||||
|
0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E,
|
||||||
|
0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x72, 0xFF, 0xFB,
|
||||||
|
0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
||||||
|
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1,
|
||||||
|
0x00, 0xE1, 0x6D, 0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB,
|
||||||
|
0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06,
|
||||||
|
0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD,
|
||||||
|
0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
||||||
|
0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x22,
|
||||||
|
0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75,
|
||||||
|
0x79, 0xF1, 0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41,
|
||||||
|
0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC,
|
||||||
|
0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73,
|
||||||
|
0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x6E, 0x73, 0xE9, 0xBA, 0xE9,
|
||||||
|
0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x00, 0xE1,
|
||||||
|
0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41,
|
||||||
|
0x74, 0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF,
|
||||||
|
0xFD, 0xF2, 0x88, 0x42, 0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73,
|
||||||
|
0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD,
|
||||||
|
0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||||
|
0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0x41, 0x6D,
|
||||||
|
0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
||||||
|
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB,
|
||||||
|
0xF2, 0x50, 0xF0, 0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42,
|
||||||
|
0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48,
|
||||||
|
0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6,
|
||||||
|
0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0x21, 0x74, 0xFC, 0x21,
|
||||||
|
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0x6D,
|
||||||
|
0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80,
|
||||||
|
0xF0, 0x80, 0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1,
|
||||||
|
0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0,
|
||||||
|
0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0,
|
||||||
|
0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0x41, 0x69, 0xFA,
|
||||||
|
0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0xFD, 0x22,
|
||||||
|
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69,
|
||||||
|
0x6F, 0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0,
|
||||||
|
0x0B, 0xF4, 0x0D, 0xFF, 0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC,
|
||||||
|
0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61,
|
||||||
|
0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||||
|
0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB,
|
||||||
|
0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
||||||
|
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0,
|
||||||
|
0xEF, 0x48, 0xF0, 0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F,
|
||||||
|
0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4,
|
||||||
|
0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65,
|
||||||
|
0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr SerializedHyphenationPatterns fr_patterns = {
|
constexpr SerializedHyphenationPatterns fr_patterns = {
|
||||||
|
0x1AF0u,
|
||||||
fr_trie_data,
|
fr_trie_data,
|
||||||
sizeof(fr_trie_data),
|
sizeof(fr_trie_data),
|
||||||
};
|
};
|
||||||
|
|||||||
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "../SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||||
|
0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x0D, 0x16, 0x0B, 0x34,
|
||||||
|
0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x01, 0x02, 0x16, 0x02,
|
||||||
|
0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0B, 0xA0, 0x00, 0x42,
|
||||||
|
0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x6D, 0xFD, 0x21, 0x69,
|
||||||
|
0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
|
0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x6D,
|
||||||
|
0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6F, 0xFD,
|
||||||
|
0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x61, 0x69, 0x6F, 0xDF,
|
||||||
|
0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||||
|
0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x75, 0xFD, 0x21,
|
||||||
|
0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x00,
|
||||||
|
0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0xA0, 0x01, 0x52, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x71, 0x21, 0x6F,
|
||||||
|
0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x6F, 0xFD,
|
||||||
|
0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x72,
|
||||||
|
0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0x6C, 0x72, 0xFD, 0xFD,
|
||||||
|
0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x11, 0x25, 0x61, 0x68,
|
||||||
|
0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x72, 0xFD, 0x21, 0x63,
|
||||||
|
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xA2, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x72, 0xFF, 0xFC, 0xFF,
|
||||||
|
0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x72,
|
||||||
|
0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0x21,
|
||||||
|
0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0xFD, 0x41, 0x6E, 0xFF,
|
||||||
|
0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x61, 0x65,
|
||||||
|
0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x12, 0xFF, 0x20, 0xFF,
|
||||||
|
0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0xC2, 0xFF, 0xE6, 0xFF,
|
||||||
|
0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xA0, 0x00, 0xD1, 0x24,
|
||||||
|
0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0xF1, 0xA0, 0x01,
|
||||||
|
0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xFD, 0x21, 0x75, 0xDF,
|
||||||
|
0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0x02, 0x01, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0xE3, 0xE3, 0xE3, 0xE3,
|
||||||
|
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0x27, 0xC4, 0xC7, 0xC6,
|
||||||
|
0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xFB, 0xFF, 0xBF,
|
||||||
|
0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x6E, 0x71, 0x73,
|
||||||
|
0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||||
|
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xEB, 0xFF,
|
||||||
|
0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0x67, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0x72, 0x73, 0x74, 0x2E,
|
||||||
|
0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x4A, 0xFF,
|
||||||
|
0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0xFD, 0xD1, 0x02, 0x01,
|
||||||
|
0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E,
|
||||||
|
0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x33, 0xFF, 0x21, 0xFF,
|
||||||
|
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
|
||||||
|
0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x68, 0x69, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0xFC, 0xFE, 0xF9, 0xFE,
|
||||||
|
0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0x02, 0x01, 0x2E, 0x27,
|
||||||
|
0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0x6D, 0x72, 0x73, 0x74,
|
||||||
|
0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
|
||||||
|
0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0x2E, 0x27, 0xFE, 0x93,
|
||||||
|
0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||||
|
0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0x21, 0x72, 0xF8, 0x21,
|
||||||
|
0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x69, 0xFC, 0x21, 0x65,
|
||||||
|
0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||||
|
0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||||
|
0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||||
|
0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x82,
|
||||||
|
0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xFD, 0xCB, 0x02, 0x01,
|
||||||
|
0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0xB1, 0xFD, 0xC3, 0xFD,
|
||||||
|
0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD,
|
||||||
|
0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0x8D, 0xA0, 0x02, 0x53,
|
||||||
|
0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x68, 0x67, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x27, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x37, 0xFD, 0x37, 0xFD,
|
||||||
|
0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x8F, 0x4B, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
|
||||||
|
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xA0,
|
||||||
|
0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x70, 0x74, 0x7A, 0x2E,
|
||||||
|
0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0xF8, 0xFF, 0xFB, 0xC1,
|
||||||
|
0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x63, 0xFC, 0xC1, 0x01,
|
||||||
|
0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0x06, 0xD2, 0x02, 0x01,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A,
|
||||||
|
0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xE2, 0xFC, 0xD3,
|
||||||
|
0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1,
|
||||||
|
0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0x76, 0x2E, 0x27, 0xFC,
|
||||||
|
0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x72, 0xFB, 0xAF, 0xA0,
|
||||||
|
0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0xFF, 0xF9, 0xFF, 0xFD,
|
||||||
|
0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0x70, 0x74, 0x77, 0x2E,
|
||||||
|
0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
|
||||||
|
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0xCB, 0x02, 0x01, 0x62,
|
||||||
|
0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
|
||||||
|
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFD, 0x9F,
|
||||||
|
0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0xC2, 0xFB, 0xF9, 0xFC,
|
||||||
|
0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0xC4, 0xFC, 0xED, 0xFD,
|
||||||
|
0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x5D, 0xFE, 0x81, 0xFE,
|
||||||
|
0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xD5, 0xFF, 0xDC,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns it_patterns = {
|
||||||
|
0x5C0u,
|
||||||
|
it_trie_data,
|
||||||
|
sizeof(it_trie_data),
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,22 @@
|
|||||||
#include "ChapterHtmlSlimParser.h"
|
#include "ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include "../../Epub.h"
|
||||||
#include "../Page.h"
|
#include "../Page.h"
|
||||||
|
#include "../converters/ImageDecoderFactory.h"
|
||||||
|
#include "../converters/ImageToFramebufferDecoder.h"
|
||||||
|
#include "../htmlEntities.h"
|
||||||
|
|
||||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||||
|
|
||||||
// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it
|
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
||||||
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||||
|
|
||||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||||
@@ -22,6 +27,9 @@ constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
|||||||
const char* ITALIC_TAGS[] = {"i", "em"};
|
const char* ITALIC_TAGS[] = {"i", "em"};
|
||||||
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
||||||
|
|
||||||
|
const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||||
|
constexpr int NUM_UNDERLINE_TAGS = sizeof(UNDERLINE_TAGS) / sizeof(UNDERLINE_TAGS[0]);
|
||||||
|
|
||||||
const char* IMAGE_TAGS[] = {"img"};
|
const char* IMAGE_TAGS[] = {"img"};
|
||||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||||
|
|
||||||
@@ -40,35 +48,74 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isHeaderOrBlock(const char* name) {
|
||||||
|
return matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update effective bold/italic/underline based on block style and inline style stack
|
||||||
|
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||||
|
// Start with block-level styles
|
||||||
|
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
|
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
|
effectiveUnderline =
|
||||||
|
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
||||||
|
|
||||||
|
// Apply inline style stack in order
|
||||||
|
for (const auto& entry : inlineStyleStack) {
|
||||||
|
if (entry.hasBold) {
|
||||||
|
effectiveBold = entry.bold;
|
||||||
|
}
|
||||||
|
if (entry.hasItalic) {
|
||||||
|
effectiveItalic = entry.italic;
|
||||||
|
}
|
||||||
|
if (entry.hasUnderline) {
|
||||||
|
effectiveUnderline = entry.underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// flush the contents of partWordBuffer to currentTextBlock
|
// flush the contents of partWordBuffer to currentTextBlock
|
||||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||||
// determine font style
|
// Determine font style from depth-based tracking and CSS effective style
|
||||||
|
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
||||||
|
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||||
|
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
||||||
|
|
||||||
|
// Combine style flags using bitwise OR
|
||||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
if (isBold) {
|
||||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::BOLD);
|
||||||
} else if (boldUntilDepth < depth) {
|
|
||||||
fontStyle = EpdFontFamily::BOLD;
|
|
||||||
} else if (italicUntilDepth < depth) {
|
|
||||||
fontStyle = EpdFontFamily::ITALIC;
|
|
||||||
}
|
}
|
||||||
|
if (isItalic) {
|
||||||
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
||||||
|
}
|
||||||
|
if (isUnderline) {
|
||||||
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
||||||
|
}
|
||||||
|
|
||||||
// flush the buffer
|
// flush the buffer
|
||||||
partWordBuffer[partWordBufferIndex] = '\0';
|
partWordBuffer[partWordBufferIndex] = '\0';
|
||||||
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||||
partWordBufferIndex = 0;
|
partWordBufferIndex = 0;
|
||||||
|
nextWordContinues = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start a new text block if needed
|
// start a new text block if needed
|
||||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||||
|
nextWordContinues = false; // New block = new paragraph, no continuation
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
// already have a text block running and it is empty - just reuse it
|
// already have a text block running and it is empty - just reuse it
|
||||||
if (currentTextBlock->isEmpty()) {
|
if (currentTextBlock->isEmpty()) {
|
||||||
currentTextBlock->setStyle(style);
|
// Merge with existing block style to accumulate CSS styling from parent block elements.
|
||||||
|
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
|
||||||
|
// div's margin should be preserved, even though it has no direct text content.
|
||||||
|
currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
makePages();
|
makePages();
|
||||||
}
|
}
|
||||||
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled));
|
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
@@ -80,13 +127,30 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract class and style attributes for CSS processing
|
||||||
|
std::string classAttr;
|
||||||
|
std::string styleAttr;
|
||||||
|
if (atts != nullptr) {
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "class") == 0) {
|
||||||
|
classAttr = atts[i + 1];
|
||||||
|
} else if (strcmp(atts[i], "style") == 0) {
|
||||||
|
styleAttr = atts[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto centeredBlockStyle = BlockStyle();
|
||||||
|
centeredBlockStyle.textAlignDefined = true;
|
||||||
|
centeredBlockStyle.alignment = CssTextAlign::Center;
|
||||||
|
|
||||||
// Special handling for tables - show placeholder text instead of dropping silently
|
// Special handling for tables - show placeholder text instead of dropping silently
|
||||||
if (strcmp(name, "table") == 0) {
|
if (strcmp(name, "table") == 0) {
|
||||||
// Add placeholder text
|
// Add placeholder text
|
||||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
self->startNewTextBlock(centeredBlockStyle);
|
||||||
|
|
||||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||||
// Advance depth before processing character data (like you would for a element with text)
|
// Advance depth before processing character data (like you would for an element with text)
|
||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||||
|
|
||||||
@@ -96,30 +160,125 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||||
// TODO: Start processing image tags
|
std::string src;
|
||||||
std::string alt = "[Image]";
|
std::string alt;
|
||||||
if (atts != nullptr) {
|
if (atts != nullptr) {
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "alt") == 0) {
|
if (strcmp(atts[i], "src") == 0) {
|
||||||
if (strlen(atts[i + 1]) > 0) {
|
src = atts[i + 1];
|
||||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
} else if (strcmp(atts[i], "alt") == 0) {
|
||||||
}
|
alt = atts[i + 1];
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!src.empty()) {
|
||||||
|
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||||
|
|
||||||
|
{
|
||||||
|
// Resolve the image path relative to the HTML file
|
||||||
|
std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src);
|
||||||
|
|
||||||
|
// Create a unique filename for the cached image
|
||||||
|
std::string ext;
|
||||||
|
size_t extPos = resolvedPath.rfind('.');
|
||||||
|
if (extPos != std::string::npos) {
|
||||||
|
ext = resolvedPath.substr(extPos);
|
||||||
|
}
|
||||||
|
std::string cachedImagePath = self->imageBasePath + std::to_string(self->imageCounter++) + ext;
|
||||||
|
|
||||||
|
// Extract image to cache file
|
||||||
|
FsFile cachedImageFile;
|
||||||
|
bool extractSuccess = false;
|
||||||
|
if (Storage.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) {
|
||||||
|
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||||
|
cachedImageFile.flush();
|
||||||
|
cachedImageFile.close();
|
||||||
|
delay(50); // Give SD card time to sync
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractSuccess) {
|
||||||
|
// Get image dimensions
|
||||||
|
ImageDimensions dims = {0, 0};
|
||||||
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||||
|
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
||||||
|
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||||
|
|
||||||
|
// Scale to fit viewport while maintaining aspect ratio
|
||||||
|
int maxWidth = self->viewportWidth;
|
||||||
|
int maxHeight = self->viewportHeight;
|
||||||
|
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
||||||
|
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
|
||||||
|
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (scale > 1.0f) scale = 1.0f;
|
||||||
|
|
||||||
|
int displayWidth = (int)(dims.width * scale);
|
||||||
|
int displayHeight = (int)(dims.height * scale);
|
||||||
|
|
||||||
|
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
|
||||||
|
|
||||||
|
// Create page for image - only break if image won't fit remaining space
|
||||||
|
if (self->currentPage && !self->currentPage->elements.empty() &&
|
||||||
|
(self->currentPageNextY + displayHeight > self->viewportHeight)) {
|
||||||
|
self->completePageFn(std::move(self->currentPage));
|
||||||
|
self->currentPage.reset(new Page());
|
||||||
|
if (!self->currentPage) {
|
||||||
|
LOG_ERR("EHP", "Failed to create new page");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->currentPageNextY = 0;
|
||||||
|
} else if (!self->currentPage) {
|
||||||
|
self->currentPage.reset(new Page());
|
||||||
|
if (!self->currentPage) {
|
||||||
|
LOG_ERR("EHP", "Failed to create initial page");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->currentPageNextY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ImageBlock and add to page
|
||||||
|
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight);
|
||||||
|
if (!imageBlock) {
|
||||||
|
LOG_ERR("EHP", "Failed to create ImageBlock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int xPos = (self->viewportWidth - displayWidth) / 2;
|
||||||
|
auto pageImage = std::make_shared<PageImage>(imageBlock, xPos, self->currentPageNextY);
|
||||||
|
if (!pageImage) {
|
||||||
|
LOG_ERR("EHP", "Failed to create PageImage");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->currentPage->elements.push_back(pageImage);
|
||||||
|
self->currentPageNextY += displayHeight;
|
||||||
|
|
||||||
|
self->depth += 1;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
LOG_ERR("EHP", "Failed to get image dimensions");
|
||||||
|
Storage.remove(cachedImagePath.c_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERR("EHP", "Failed to extract image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to alt text if image processing fails
|
||||||
|
if (!alt.empty()) {
|
||||||
|
alt = "[Image: " + alt + "]";
|
||||||
|
self->startNewTextBlock(centeredBlockStyle);
|
||||||
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||||
|
self->depth += 1;
|
||||||
|
self->characterData(userData, alt.c_str(), alt.length());
|
||||||
|
// Skip any child content (skip until parent as we pre-advanced depth above)
|
||||||
|
self->skipUntilDepth = self->depth - 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No alt text, skip
|
||||||
|
self->skipUntilDepth = self->depth;
|
||||||
|
self->depth += 1;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
|
||||||
|
|
||||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
|
||||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
|
||||||
// Advance depth before processing character data (like you would for a element with text)
|
|
||||||
self->depth += 1;
|
|
||||||
self->characterData(userData, alt.c_str(), alt.length());
|
|
||||||
|
|
||||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
|
||||||
self->skipUntilDepth = self->depth - 1;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||||
@@ -141,43 +300,139 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
// Compute CSS style for this element
|
||||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
CssStyle cssStyle;
|
||||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
if (self->cssParser) {
|
||||||
self->depth += 1;
|
// Get combined tag + class styles
|
||||||
return;
|
cssStyle = self->cssParser->resolveStyle(name, classAttr);
|
||||||
|
// Merge inline style (highest priority)
|
||||||
|
if (!styleAttr.empty()) {
|
||||||
|
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||||
|
cssStyle.applyOver(inlineStyle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
|
||||||
|
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
|
||||||
|
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
|
||||||
|
|
||||||
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||||
|
self->currentCssStyle = cssStyle;
|
||||||
|
auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth);
|
||||||
|
headerBlockStyle.textAlignDefined = true;
|
||||||
|
if (self->embeddedStyle && cssStyle.hasTextAlign()) {
|
||||||
|
headerBlockStyle.alignment = cssStyle.textAlign;
|
||||||
|
}
|
||||||
|
self->startNewTextBlock(headerBlockStyle);
|
||||||
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
if (strcmp(name, "br") == 0) {
|
if (strcmp(name, "br") == 0) {
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
|
||||||
self->depth += 1;
|
} else {
|
||||||
return;
|
self->currentCssStyle = cssStyle;
|
||||||
|
self->startNewTextBlock(userAlignmentBlockStyle);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
|
||||||
|
if (strcmp(name, "li") == 0) {
|
||||||
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||||
self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment));
|
// Flush buffer before style change so preceding text gets current style
|
||||||
if (strcmp(name, "li") == 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
self->flushPartWordBuffer();
|
||||||
|
self->nextWordContinues = true;
|
||||||
|
}
|
||||||
|
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||||
|
// Push inline style entry for underline tag
|
||||||
|
StyleStackEntry entry;
|
||||||
|
entry.depth = self->depth; // Track depth for matching pop
|
||||||
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = true;
|
||||||
|
if (cssStyle.hasFontWeight()) {
|
||||||
|
entry.hasBold = true;
|
||||||
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasFontStyle()) {
|
||||||
|
entry.hasItalic = true;
|
||||||
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
|
}
|
||||||
|
self->inlineStyleStack.push_back(entry);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||||
|
// Flush buffer before style change so preceding text gets current style
|
||||||
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
self->nextWordContinues = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->depth += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
|
||||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
self->depth += 1;
|
// Push inline style entry for bold tag
|
||||||
return;
|
StyleStackEntry entry;
|
||||||
}
|
entry.depth = self->depth; // Track depth for matching pop
|
||||||
|
entry.hasBold = true;
|
||||||
if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
entry.bold = true;
|
||||||
|
if (cssStyle.hasFontStyle()) {
|
||||||
|
entry.hasItalic = true;
|
||||||
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasTextDecoration()) {
|
||||||
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||||
|
}
|
||||||
|
self->inlineStyleStack.push_back(entry);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||||
|
// Flush buffer before style change so preceding text gets current style
|
||||||
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
self->nextWordContinues = true;
|
||||||
|
}
|
||||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||||
self->depth += 1;
|
// Push inline style entry for italic tag
|
||||||
return;
|
StyleStackEntry entry;
|
||||||
|
entry.depth = self->depth; // Track depth for matching pop
|
||||||
|
entry.hasItalic = true;
|
||||||
|
entry.italic = true;
|
||||||
|
if (cssStyle.hasFontWeight()) {
|
||||||
|
entry.hasBold = true;
|
||||||
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasTextDecoration()) {
|
||||||
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||||
|
}
|
||||||
|
self->inlineStyleStack.push_back(entry);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
} else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) {
|
||||||
|
// Handle span and other inline elements for CSS styling
|
||||||
|
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
||||||
|
// Flush buffer before style change so preceding text gets current style
|
||||||
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
self->nextWordContinues = true;
|
||||||
|
}
|
||||||
|
StyleStackEntry entry;
|
||||||
|
entry.depth = self->depth; // Track depth for matching pop
|
||||||
|
if (cssStyle.hasFontWeight()) {
|
||||||
|
entry.hasBold = true;
|
||||||
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasFontStyle()) {
|
||||||
|
entry.hasItalic = true;
|
||||||
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasTextDecoration()) {
|
||||||
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||||
|
}
|
||||||
|
self->inlineStyleStack.push_back(entry);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unprocessed tag, just increasing depth and continue forward
|
// Unprocessed tag, just increasing depth and continue forward
|
||||||
@@ -198,10 +453,34 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
|
// Whitespace is a real word boundary — reset continuation state
|
||||||
|
self->nextWordContinues = false;
|
||||||
// Skip the whitespace char
|
// Skip the whitespace char
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect U+00A0 (non-breaking space): UTF-8 encoding is 0xC2 0xA0
|
||||||
|
// Render a visible space without allowing a line break around it.
|
||||||
|
if (static_cast<uint8_t>(s[i]) == 0xC2 && i + 1 < len && static_cast<uint8_t>(s[i + 1]) == 0xA0) {
|
||||||
|
// Flush any pending text so style is applied correctly.
|
||||||
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a standalone space that attaches to the previous word.
|
||||||
|
self->partWordBuffer[0] = ' ';
|
||||||
|
self->partWordBuffer[1] = '\0';
|
||||||
|
self->partWordBufferIndex = 1;
|
||||||
|
self->nextWordContinues = true; // Attach space to previous word (no break).
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
|
||||||
|
// Ensure the next real word attaches to this space (no break).
|
||||||
|
self->nextWordContinues = true;
|
||||||
|
|
||||||
|
i++; // Skip the second byte (0xA0)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
|
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
|
||||||
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
|
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
|
||||||
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
|
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
|
||||||
@@ -229,28 +508,60 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
// memory.
|
// memory.
|
||||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||||
if (self->currentTextBlock->size() > 750) {
|
if (self->currentTextBlock->size() > 750) {
|
||||||
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
|
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||||
self->currentTextBlock->layoutAndExtractLines(
|
self->currentTextBlock->layoutAndExtractLines(
|
||||||
self->renderer, self->fontId, self->viewportWidth,
|
self->renderer, self->fontId, self->viewportWidth,
|
||||||
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
|
||||||
|
// Check if this looks like an entity reference (&...;)
|
||||||
|
if (len >= 3 && s[0] == '&' && s[len - 1] == ';') {
|
||||||
|
const char* utf8Value = lookupHtmlEntity(s, len);
|
||||||
|
if (utf8Value != nullptr) {
|
||||||
|
// Known entity: expand to its UTF-8 value
|
||||||
|
characterData(userData, utf8Value, strlen(utf8Value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unknown entity: preserve original &...; sequence
|
||||||
|
characterData(userData, s, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Not an entity we recognize - skip it
|
||||||
|
}
|
||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
|
|
||||||
if (self->partWordBufferIndex > 0) {
|
// Check if any style state will change after we decrement depth
|
||||||
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
// If so, we MUST flush the partWordBuffer with the CURRENT style first
|
||||||
// We don't want to flush out content when closing inline tags like <span>.
|
// Note: depth hasn't been decremented yet, so we check against (depth - 1)
|
||||||
// Currently this also flushes out on closing <b> and <i> tags, but they are line tags so that shouldn't happen,
|
const bool willPopStyleStack =
|
||||||
// text styling needs to be overhauled to fix it.
|
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
||||||
const bool shouldBreakText =
|
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
||||||
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
||||||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
||||||
strcmp(name, "table") == 0 || matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
|
||||||
|
|
||||||
if (shouldBreakText) {
|
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||||
|
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||||
|
|
||||||
|
// Flush buffer with current style BEFORE any style changes
|
||||||
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
// Flush if style will change OR if we're closing a block/structural element
|
||||||
|
const bool isInlineTag = !headerOrBlockTag && strcmp(name, "table") != 0 &&
|
||||||
|
!matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
|
||||||
|
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||||
|
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||||
|
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 ||
|
||||||
|
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||||
|
|
||||||
|
if (shouldFlush) {
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
|
// If closing an inline element, the next word fragment continues the same visual word
|
||||||
|
if (isInlineTag) {
|
||||||
|
self->nextWordContinues = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,38 +572,67 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->skipUntilDepth = INT_MAX;
|
self->skipUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaving bold
|
// Leaving bold tag
|
||||||
if (self->boldUntilDepth == self->depth) {
|
if (self->boldUntilDepth == self->depth) {
|
||||||
self->boldUntilDepth = INT_MAX;
|
self->boldUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaving italic
|
// Leaving italic tag
|
||||||
if (self->italicUntilDepth == self->depth) {
|
if (self->italicUntilDepth == self->depth) {
|
||||||
self->italicUntilDepth = INT_MAX;
|
self->italicUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaving underline tag
|
||||||
|
if (self->underlineUntilDepth == self->depth) {
|
||||||
|
self->underlineUntilDepth = INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop from inline style stack if we pushed an entry at this depth
|
||||||
|
// This handles all inline elements: b, i, u, span, etc.
|
||||||
|
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
||||||
|
self->inlineStyleStack.pop_back();
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear block style when leaving header or block elements
|
||||||
|
if (headerOrBlockTag) {
|
||||||
|
self->currentCssStyle.reset();
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||||
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
auto paragraphAlignmentBlockStyle = BlockStyle();
|
||||||
|
paragraphAlignmentBlockStyle.textAlignDefined = true;
|
||||||
|
// Resolve None sentinel to Justify for initial block (no CSS context yet)
|
||||||
|
const auto align = (this->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
|
||||||
|
? CssTextAlign::Justify
|
||||||
|
: static_cast<CssTextAlign>(this->paragraphAlignment);
|
||||||
|
paragraphAlignmentBlockStyle.alignment = align;
|
||||||
|
startNewTextBlock(paragraphAlignmentBlockStyle);
|
||||||
|
|
||||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||||
int done;
|
int done;
|
||||||
|
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
||||||
|
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
||||||
|
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand);
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("EHP", filepath, file)) {
|
if (!Storage.openFileForRead("EHP", filepath, file)) {
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file size for progress calculation
|
// Get file size to decide whether to show indexing popup.
|
||||||
const size_t totalSize = file.size();
|
if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
|
||||||
size_t bytesRead = 0;
|
popupFn();
|
||||||
int lastProgress = -1;
|
}
|
||||||
|
|
||||||
XML_SetUserData(parser, this);
|
XML_SetUserData(parser, this);
|
||||||
XML_SetElementHandler(parser, startElement, endElement);
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
@@ -301,7 +641,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
do {
|
do {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -313,7 +653,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
const size_t len = file.read(buf, 1024);
|
const size_t len = file.read(buf, 1024);
|
||||||
|
|
||||||
if (len == 0 && file.available() > 0) {
|
if (len == 0 && file.available() > 0) {
|
||||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
LOG_ERR("EHP", "File read error");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -322,22 +662,11 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update progress (call every 10% change to avoid too frequent updates)
|
|
||||||
// Only show progress for larger chapters where rendering overhead is worth it
|
|
||||||
bytesRead += len;
|
|
||||||
if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) {
|
|
||||||
const int progress = static_cast<int>((bytesRead * 100) / totalSize);
|
|
||||||
if (lastProgress / 10 != progress / 10) {
|
|
||||||
lastProgress = progress;
|
|
||||||
progressFn(progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
done = file.available() == 0;
|
done = file.available() == 0;
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -373,13 +702,15 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
|||||||
currentPageNextY = 0;
|
currentPageNextY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPage->elements.push_back(std::make_shared<PageLine>(line, 0, currentPageNextY));
|
// Apply horizontal left inset (margin + padding) as x position offset
|
||||||
|
const int16_t xOffset = line->getBlockStyle().leftInset();
|
||||||
|
currentPage->elements.push_back(std::make_shared<PageLine>(line, xOffset, currentPageNextY));
|
||||||
currentPageNextY += lineHeight;
|
currentPageNextY += lineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChapterHtmlSlimParser::makePages() {
|
void ChapterHtmlSlimParser::makePages() {
|
||||||
if (!currentTextBlock) {
|
if (!currentTextBlock) {
|
||||||
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
LOG_ERR("EHP", "!! No text block to make pages for !!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,10 +720,34 @@ void ChapterHtmlSlimParser::makePages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||||
|
|
||||||
|
// Apply top spacing before the paragraph (stored in pixels)
|
||||||
|
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
||||||
|
if (blockStyle.marginTop > 0) {
|
||||||
|
currentPageNextY += blockStyle.marginTop;
|
||||||
|
}
|
||||||
|
if (blockStyle.paddingTop > 0) {
|
||||||
|
currentPageNextY += blockStyle.paddingTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate effective width accounting for horizontal margins/padding
|
||||||
|
const int horizontalInset = blockStyle.totalHorizontalInset();
|
||||||
|
const uint16_t effectiveWidth =
|
||||||
|
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||||
|
|
||||||
currentTextBlock->layoutAndExtractLines(
|
currentTextBlock->layoutAndExtractLines(
|
||||||
renderer, fontId, viewportWidth,
|
renderer, fontId, effectiveWidth,
|
||||||
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
||||||
// Extra paragraph spacing if enabled
|
|
||||||
|
// Apply bottom spacing after the paragraph (stored in pixels)
|
||||||
|
if (blockStyle.marginBottom > 0) {
|
||||||
|
currentPageNextY += blockStyle.marginBottom;
|
||||||
|
}
|
||||||
|
if (blockStyle.paddingBottom > 0) {
|
||||||
|
currentPageNextY += blockStyle.paddingBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra paragraph spacing if enabled (default behavior)
|
||||||
if (extraParagraphSpacing) {
|
if (extraParagraphSpacing) {
|
||||||
currentPageNextY += lineHeight / 2;
|
currentPageNextY += lineHeight / 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,26 +7,33 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../ParsedText.h"
|
#include "../ParsedText.h"
|
||||||
|
#include "../blocks/ImageBlock.h"
|
||||||
#include "../blocks/TextBlock.h"
|
#include "../blocks/TextBlock.h"
|
||||||
|
#include "../css/CssParser.h"
|
||||||
|
#include "../css/CssStyle.h"
|
||||||
|
|
||||||
class Page;
|
class Page;
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
|
class Epub;
|
||||||
|
|
||||||
#define MAX_WORD_SIZE 200
|
#define MAX_WORD_SIZE 200
|
||||||
|
|
||||||
class ChapterHtmlSlimParser {
|
class ChapterHtmlSlimParser {
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
const std::string& filepath;
|
const std::string& filepath;
|
||||||
GfxRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||||
std::function<void(int)> progressFn; // Progress callback (0-100)
|
std::function<void()> popupFn; // Popup callback
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
int skipUntilDepth = INT_MAX;
|
int skipUntilDepth = INT_MAX;
|
||||||
int boldUntilDepth = INT_MAX;
|
int boldUntilDepth = INT_MAX;
|
||||||
int italicUntilDepth = INT_MAX;
|
int italicUntilDepth = INT_MAX;
|
||||||
|
int underlineUntilDepth = INT_MAX;
|
||||||
// buffer for building up words from characters, will auto break if longer than this
|
// buffer for building up words from characters, will auto break if longer than this
|
||||||
// leave one char at end for null pointer
|
// leave one char at end for null pointer
|
||||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||||
int partWordBufferIndex = 0;
|
int partWordBufferIndex = 0;
|
||||||
|
bool nextWordContinues = false; // true when next flushed word attaches to previous (inline element boundary)
|
||||||
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
|
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
|
||||||
std::unique_ptr<Page> currentPage = nullptr;
|
std::unique_ptr<Page> currentPage = nullptr;
|
||||||
int16_t currentPageNextY = 0;
|
int16_t currentPageNextY = 0;
|
||||||
@@ -37,23 +44,47 @@ class ChapterHtmlSlimParser {
|
|||||||
uint16_t viewportWidth;
|
uint16_t viewportWidth;
|
||||||
uint16_t viewportHeight;
|
uint16_t viewportHeight;
|
||||||
bool hyphenationEnabled;
|
bool hyphenationEnabled;
|
||||||
|
const CssParser* cssParser;
|
||||||
|
bool embeddedStyle;
|
||||||
|
std::string contentBase;
|
||||||
|
std::string imageBasePath;
|
||||||
|
int imageCounter = 0;
|
||||||
|
|
||||||
void startNewTextBlock(TextBlock::Style style);
|
// Style tracking (replaces depth-based approach)
|
||||||
|
struct StyleStackEntry {
|
||||||
|
int depth = 0;
|
||||||
|
bool hasBold = false, bold = false;
|
||||||
|
bool hasItalic = false, italic = false;
|
||||||
|
bool hasUnderline = false, underline = false;
|
||||||
|
};
|
||||||
|
std::vector<StyleStackEntry> inlineStyleStack;
|
||||||
|
CssStyle currentCssStyle;
|
||||||
|
bool effectiveBold = false;
|
||||||
|
bool effectiveItalic = false;
|
||||||
|
bool effectiveUnderline = false;
|
||||||
|
|
||||||
|
void updateEffectiveInlineStyle();
|
||||||
|
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||||
void flushPartWordBuffer();
|
void flushPartWordBuffer();
|
||||||
void makePages();
|
void makePages();
|
||||||
// XML callbacks
|
// XML callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||||
|
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
|
||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
|
||||||
const float lineCompression, const bool extraParagraphSpacing,
|
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||||
const std::function<void(int)>& progressFn = nullptr)
|
const bool embeddedStyle, const std::string& contentBase,
|
||||||
: filepath(filepath),
|
const std::string& imageBasePath, const std::function<void()>& popupFn = nullptr,
|
||||||
|
const CssParser* cssParser = nullptr)
|
||||||
|
|
||||||
|
: epub(epub),
|
||||||
|
filepath(filepath),
|
||||||
renderer(renderer),
|
renderer(renderer),
|
||||||
fontId(fontId),
|
fontId(fontId),
|
||||||
lineCompression(lineCompression),
|
lineCompression(lineCompression),
|
||||||
@@ -63,7 +94,12 @@ class ChapterHtmlSlimParser {
|
|||||||
viewportHeight(viewportHeight),
|
viewportHeight(viewportHeight),
|
||||||
hyphenationEnabled(hyphenationEnabled),
|
hyphenationEnabled(hyphenationEnabled),
|
||||||
completePageFn(completePageFn),
|
completePageFn(completePageFn),
|
||||||
progressFn(progressFn) {}
|
popupFn(popupFn),
|
||||||
|
cssParser(cssParser),
|
||||||
|
embeddedStyle(embeddedStyle),
|
||||||
|
contentBase(contentBase),
|
||||||
|
imageBasePath(imageBasePath) {}
|
||||||
|
|
||||||
~ChapterHtmlSlimParser() = default;
|
~ChapterHtmlSlimParser() = default;
|
||||||
bool parseAndBuildPages();
|
bool parseAndBuildPages();
|
||||||
void addLineToPage(std::shared_ptr<TextBlock> line);
|
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "ContainerParser.h"
|
#include "ContainerParser.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
bool ContainerParser::setup() {
|
bool ContainerParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis());
|
LOG_ERR("CTR", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
while (remainingInBuffer > 0) {
|
while (remainingInBuffer > 0) {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis());
|
LOG_DBG("CTR", "Couldn't allocate buffer");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser)));
|
LOG_ERR("CTR", "Parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
#include "ContentOpfParser.h"
|
#include "ContentOpfParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "../BookMetadataCache.h"
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||||
|
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||||
constexpr char itemCacheFile[] = "/.items.bin";
|
constexpr char itemCacheFile[] = "/.items.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool ContentOpfParser::setup() {
|
bool ContentOpfParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("COF", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +36,8 @@ ContentOpfParser::~ContentOpfParser() {
|
|||||||
if (tempItemStore) {
|
if (tempItemStore) {
|
||||||
tempItemStore.close();
|
tempItemStore.close();
|
||||||
}
|
}
|
||||||
if (SdMan.exists((cachePath + itemCacheFile).c_str())) {
|
if (Storage.exists((cachePath + itemCacheFile).c_str())) {
|
||||||
SdMan.remove((cachePath + itemCacheFile).c_str());
|
Storage.remove((cachePath + itemCacheFile).c_str());
|
||||||
}
|
}
|
||||||
itemIndex.clear();
|
itemIndex.clear();
|
||||||
itemIndex.shrink_to_fit();
|
itemIndex.shrink_to_fit();
|
||||||
@@ -55,7 +56,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
|
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
|
LOG_ERR("COF", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -68,8 +69,8 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("COF", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -117,20 +118,16 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
|
|
||||||
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
self->state = IN_MANIFEST;
|
self->state = IN_MANIFEST;
|
||||||
if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
Serial.printf(
|
LOG_ERR("COF", "Couldn't open temp items file for writing. This is probably going to be a fatal error.");
|
||||||
"[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n",
|
|
||||||
millis());
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||||
self->state = IN_SPINE;
|
self->state = IN_SPINE;
|
||||||
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
Serial.printf(
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
|
||||||
millis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort item index for binary search if we have enough items
|
// Sort item index for binary search if we have enough items
|
||||||
@@ -139,7 +136,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||||
});
|
});
|
||||||
self->useItemIndex = true;
|
self->useItemIndex = true;
|
||||||
Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size());
|
LOG_DBG("COF", "Using fast index for %zu manifest items", self->itemIndex.size());
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -147,11 +144,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
self->state = IN_GUIDE;
|
self->state = IN_GUIDE;
|
||||||
// TODO Remove print
|
// TODO Remove print
|
||||||
Serial.printf("[%lu] [COF] Entering guide state.\n", millis());
|
LOG_DBG("COF", "Entering guide state.");
|
||||||
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
Serial.printf(
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
|
||||||
millis());
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -213,17 +208,29 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
if (self->tocNcxPath.empty()) {
|
if (self->tocNcxPath.empty()) {
|
||||||
self->tocNcxPath = href;
|
self->tocNcxPath = href;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(),
|
LOG_DBG("COF", "Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s", href.c_str());
|
||||||
href.c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect CSS files
|
||||||
|
if (mediaType == MEDIA_TYPE_CSS) {
|
||||||
|
self->cssFiles.push_back(href);
|
||||||
|
}
|
||||||
|
|
||||||
// EPUB 3: Check for nav document (properties contains "nav")
|
// EPUB 3: Check for nav document (properties contains "nav")
|
||||||
if (!properties.empty() && self->tocNavPath.empty()) {
|
if (!properties.empty() && self->tocNavPath.empty()) {
|
||||||
// Properties is space-separated, check if "nav" is present as a word
|
// Properties is space-separated, check if "nav" is present as a word
|
||||||
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
||||||
self->tocNavPath = href;
|
self->tocNavPath = href;
|
||||||
Serial.printf("[%lu] [COF] Found EPUB 3 nav document: %s\n", millis(), href.c_str());
|
LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EPUB 3: Check for cover image (properties contains "cover-image")
|
||||||
|
if (!properties.empty() && self->coverItemHref.empty()) {
|
||||||
|
if (properties == "cover-image" || properties.find("cover-image ") == 0 ||
|
||||||
|
properties.find(" cover-image") != std::string::npos) {
|
||||||
|
self->coverItemHref = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -289,23 +296,22 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
// parse the guide
|
// parse the guide
|
||||||
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
||||||
std::string type;
|
std::string type;
|
||||||
std::string textHref;
|
std::string guideHref;
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "type") == 0) {
|
if (strcmp(atts[i], "type") == 0) {
|
||||||
type = atts[i + 1];
|
type = atts[i + 1];
|
||||||
if (type == "text" || type == "start") {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (strcmp(atts[i], "href") == 0) {
|
} 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)) {
|
if (!guideHref.empty()) {
|
||||||
Serial.printf("[%lu] [COF] Found %s reference in guide: %s.\n", millis(), type.c_str(), textHref.c_str());
|
if (type == "text" || (type == "start" && !self->textReferenceHref.empty())) {
|
||||||
self->textReferenceHref = textHref;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -320,6 +326,9 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self->state == IN_BOOK_AUTHOR) {
|
if (self->state == IN_BOOK_AUTHOR) {
|
||||||
|
if (!self->author.empty()) {
|
||||||
|
self->author.append(", "); // Add separator for multiple authors
|
||||||
|
}
|
||||||
self->author.append(s, len);
|
self->author.append(s, len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ class ContentOpfParser final : public Print {
|
|||||||
std::string tocNcxPath;
|
std::string tocNcxPath;
|
||||||
std::string tocNavPath; // EPUB 3 nav document path
|
std::string tocNavPath; // EPUB 3 nav document path
|
||||||
std::string coverItemHref;
|
std::string coverItemHref;
|
||||||
|
std::string guideCoverPageHref; // Guide reference with type="cover" or "cover-page" (points to XHTML wrapper)
|
||||||
std::string textReferenceHref;
|
std::string textReferenceHref;
|
||||||
|
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
||||||
|
|
||||||
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||||
BookMetadataCache* cache)
|
BookMetadataCache* cache)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "TocNavParser.h"
|
#include "TocNavParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "../BookMetadataCache.h"
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
bool TocNavParser::setup() {
|
bool TocNavParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [NAV] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("NAV", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
while (remainingInBuffer > 0) {
|
while (remainingInBuffer > 0) {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [NAV] Couldn't allocate memory for buffer\n", millis());
|
LOG_DBG("NAV", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE);
|
XML_StopParser(parser, XML_FALSE);
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr);
|
XML_SetElementHandler(parser, nullptr, nullptr);
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -52,8 +52,8 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [NAV] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("NAV", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE);
|
XML_StopParser(parser, XML_FALSE);
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr);
|
XML_SetElementHandler(parser, nullptr, nullptr);
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -88,7 +88,7 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co
|
|||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
||||||
self->state = IN_NAV_TOC;
|
self->state = IN_NAV_TOC;
|
||||||
Serial.printf("[%lu] [NAV] Found nav toc element\n", millis());
|
LOG_DBG("NAV", "Found nav toc element");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
|
|||||||
|
|
||||||
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
||||||
self->state = IN_BODY;
|
self->state = IN_BODY;
|
||||||
Serial.printf("[%lu] [NAV] Finished parsing nav toc\n", millis());
|
LOG_DBG("NAV", "Finished parsing nav toc");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "TocNcxParser.h"
|
#include "TocNcxParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "../BookMetadataCache.h"
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
bool TocNcxParser::setup() {
|
bool TocNcxParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("TOC", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
while (remainingInBuffer > 0) {
|
while (remainingInBuffer > 0) {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
|
LOG_DBG("TOC", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -52,8 +52,8 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("TOC", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +1,70 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
void GfxRenderer::begin() {
|
||||||
|
frameBuffer = display.getFrameBuffer();
|
||||||
|
if (!frameBuffer) {
|
||||||
|
LOG_ERR("GFX", "!! No framebuffer");
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||||
|
|
||||||
void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
|
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||||
|
// This should always be inlined for better performance
|
||||||
|
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
||||||
|
int* phyY) {
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Portrait: {
|
case GfxRenderer::Portrait: {
|
||||||
// Logical portrait (480x800) → panel (800x480)
|
// Logical portrait (480x800) → panel (800x480)
|
||||||
// Rotation: 90 degrees clockwise
|
// Rotation: 90 degrees clockwise
|
||||||
*rotatedX = y;
|
*phyX = y;
|
||||||
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case LandscapeClockwise: {
|
case GfxRenderer::LandscapeClockwise: {
|
||||||
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
||||||
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x;
|
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
||||||
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y;
|
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PortraitInverted: {
|
case GfxRenderer::PortraitInverted: {
|
||||||
// Logical portrait (480x800) → panel (800x480)
|
// Logical portrait (480x800) → panel (800x480)
|
||||||
// Rotation: 90 degrees counter-clockwise
|
// Rotation: 90 degrees counter-clockwise
|
||||||
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y;
|
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
||||||
*rotatedY = x;
|
*phyY = x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case LandscapeCounterClockwise: {
|
case GfxRenderer::LandscapeCounterClockwise: {
|
||||||
// Logical landscape (800x480) aligned with panel orientation
|
// Logical landscape (800x480) aligned with panel orientation
|
||||||
*rotatedX = x;
|
*phyX = x;
|
||||||
*rotatedY = y;
|
*phyY = y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and
|
||||||
|
// efficient as possible.
|
||||||
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
int phyX = 0;
|
||||||
|
int phyY = 0;
|
||||||
|
|
||||||
// Early return if no framebuffer is set
|
// Note: this call should be inlined for better performance
|
||||||
if (!frameBuffer) {
|
rotateCoordinates(orientation, x, y, &phyX, &phyY);
|
||||||
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int rotatedX = 0;
|
|
||||||
int rotatedY = 0;
|
|
||||||
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
|
||||||
|
|
||||||
// Bounds checking against physical panel dimensions
|
// Bounds checking against physical panel dimensions
|
||||||
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||||
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
|
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
||||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate byte position and bit position
|
// Calculate byte position and bit position
|
||||||
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX / 8);
|
||||||
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||||
@@ -68,7 +75,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
|
|
||||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +101,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto font = fontMap.at(fontId);
|
const auto font = fontMap.at(fontId);
|
||||||
@@ -127,7 +134,13 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
Serial.printf("[%lu] [GFX] Line drawing not supported\n", millis());
|
LOG_ERR("GFX", "Line drawing not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const int lineWidth, const bool state) const {
|
||||||
|
for (int i = 0; i < lineWidth; i++) {
|
||||||
|
drawLine(x1, y1 + i, x2, y2 + i, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,16 +151,240 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
|
|||||||
drawLine(x, y, x, y + height - 1, state);
|
drawLine(x, y, x, y + height - 1, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Border is inside the rectangle
|
||||||
|
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth,
|
||||||
|
const bool state) const {
|
||||||
|
for (int i = 0; i < lineWidth; i++) {
|
||||||
|
drawLine(x + i, y + i, x + width - i, y + i, state);
|
||||||
|
drawLine(x + width - i, y + i, x + width - i, y + height - i, state);
|
||||||
|
drawLine(x + width - i, y + height - i, x + i, y + height - i, state);
|
||||||
|
drawLine(x + i, y + height - i, x + i, y + i, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir,
|
||||||
|
const int lineWidth, const bool state) const {
|
||||||
|
const int stroke = std::min(lineWidth, maxRadius);
|
||||||
|
const int innerRadius = std::max(maxRadius - stroke, 0);
|
||||||
|
const int outerRadiusSq = maxRadius * maxRadius;
|
||||||
|
const int innerRadiusSq = innerRadius * innerRadius;
|
||||||
|
for (int dy = 0; dy <= maxRadius; ++dy) {
|
||||||
|
for (int dx = 0; dx <= maxRadius; ++dx) {
|
||||||
|
const int distSq = dx * dx + dy * dy;
|
||||||
|
if (distSq > outerRadiusSq || distSq < innerRadiusSq) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int px = cx + xDir * dx;
|
||||||
|
const int py = cy + yDir * dy;
|
||||||
|
drawPixel(px, py, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Border is inside the rectangle, rounded corners
|
||||||
|
void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth,
|
||||||
|
const int cornerRadius, bool state) const {
|
||||||
|
drawRoundedRect(x, y, width, height, lineWidth, cornerRadius, true, true, true, true, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border is inside the rectangle, rounded corners
|
||||||
|
void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth,
|
||||||
|
const int cornerRadius, bool roundTopLeft, bool roundTopRight, bool roundBottomLeft,
|
||||||
|
bool roundBottomRight, bool state) const {
|
||||||
|
if (lineWidth <= 0 || width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxRadius = std::min({cornerRadius, width / 2, height / 2});
|
||||||
|
if (maxRadius <= 0) {
|
||||||
|
drawRect(x, y, width, height, lineWidth, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int stroke = std::min(lineWidth, maxRadius);
|
||||||
|
const int right = x + width - 1;
|
||||||
|
const int bottom = y + height - 1;
|
||||||
|
|
||||||
|
const int horizontalWidth = width - 2 * maxRadius;
|
||||||
|
if (horizontalWidth > 0) {
|
||||||
|
if (roundTopLeft || roundTopRight) {
|
||||||
|
fillRect(x + maxRadius, y, horizontalWidth, stroke, state);
|
||||||
|
}
|
||||||
|
if (roundBottomLeft || roundBottomRight) {
|
||||||
|
fillRect(x + maxRadius, bottom - stroke + 1, horizontalWidth, stroke, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int verticalHeight = height - 2 * maxRadius;
|
||||||
|
if (verticalHeight > 0) {
|
||||||
|
if (roundTopLeft || roundBottomLeft) {
|
||||||
|
fillRect(x, y + maxRadius, stroke, verticalHeight, state);
|
||||||
|
}
|
||||||
|
if (roundTopRight || roundBottomRight) {
|
||||||
|
fillRect(right - stroke + 1, y + maxRadius, stroke, verticalHeight, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundTopLeft) {
|
||||||
|
drawArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, lineWidth, state);
|
||||||
|
}
|
||||||
|
if (roundTopRight) {
|
||||||
|
drawArc(maxRadius, right - maxRadius, y + maxRadius, 1, -1, lineWidth, state);
|
||||||
|
}
|
||||||
|
if (roundBottomRight) {
|
||||||
|
drawArc(maxRadius, right - maxRadius, bottom - maxRadius, 1, 1, lineWidth, state);
|
||||||
|
}
|
||||||
|
if (roundBottomLeft) {
|
||||||
|
drawArc(maxRadius, x + maxRadius, bottom - maxRadius, -1, 1, lineWidth, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
for (int fillY = y; fillY < y + height; fillY++) {
|
||||||
drawLine(x, fillY, x + width - 1, fillY, state);
|
drawLine(x, fillY, x + width - 1, fillY, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Those are in critical path, and need to be templated to avoid runtime checks for every pixel.
|
||||||
|
// Any branching must be done outside the loops to avoid performance degradation.
|
||||||
|
template <>
|
||||||
|
void GfxRenderer::drawPixelDither<Color::Clear>(const int x, const int y) const {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void GfxRenderer::drawPixelDither<Color::Black>(const int x, const int y) const {
|
||||||
|
drawPixel(x, y, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void GfxRenderer::drawPixelDither<Color::White>(const int x, const int y) const {
|
||||||
|
drawPixel(x, y, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void GfxRenderer::drawPixelDither<Color::LightGray>(const int x, const int y) const {
|
||||||
|
drawPixel(x, y, x % 2 == 0 && y % 2 == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) const {
|
||||||
|
drawPixel(x, y, (x + y) % 2 == 0); // TODO: maybe find a better pattern?
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
||||||
|
if (color == Color::Clear) {
|
||||||
|
} else if (color == Color::Black) {
|
||||||
|
fillRect(x, y, width, height, true);
|
||||||
|
} else if (color == Color::White) {
|
||||||
|
fillRect(x, y, width, height, false);
|
||||||
|
} else if (color == Color::LightGray) {
|
||||||
|
for (int fillY = y; fillY < y + height; fillY++) {
|
||||||
|
for (int fillX = x; fillX < x + width; fillX++) {
|
||||||
|
drawPixelDither<Color::LightGray>(fillX, fillY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (color == Color::DarkGray) {
|
||||||
|
for (int fillY = y; fillY < y + height; fillY++) {
|
||||||
|
for (int fillX = x; fillX < x + width; fillX++) {
|
||||||
|
drawPixelDither<Color::DarkGray>(fillX, fillY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <Color color>
|
||||||
|
void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) const {
|
||||||
|
const int radiusSq = maxRadius * maxRadius;
|
||||||
|
for (int dy = 0; dy <= maxRadius; ++dy) {
|
||||||
|
for (int dx = 0; dx <= maxRadius; ++dx) {
|
||||||
|
const int distSq = dx * dx + dy * dy;
|
||||||
|
const int px = cx + xDir * dx;
|
||||||
|
const int py = cy + yDir * dy;
|
||||||
|
if (distSq <= radiusSq) {
|
||||||
|
drawPixelDither<color>(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, const int height, const int cornerRadius,
|
||||||
|
const Color color) const {
|
||||||
|
fillRoundedRect(x, y, width, height, cornerRadius, true, true, true, true, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, const int height, const int cornerRadius,
|
||||||
|
bool roundTopLeft, bool roundTopRight, bool roundBottomLeft, bool roundBottomRight,
|
||||||
|
const Color color) const {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxRadius = std::min({cornerRadius, width / 2, height / 2});
|
||||||
|
if (maxRadius <= 0) {
|
||||||
|
fillRectDither(x, y, width, height, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int horizontalWidth = width - 2 * maxRadius;
|
||||||
|
if (horizontalWidth > 0) {
|
||||||
|
fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int verticalHeight = height - 2 * maxRadius - 2;
|
||||||
|
if (verticalHeight > 0) {
|
||||||
|
fillRectDither(x, y + maxRadius + 1, maxRadius + 1, verticalHeight, color);
|
||||||
|
fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) {
|
||||||
|
switch (color) {
|
||||||
|
case Color::Clear:
|
||||||
|
break;
|
||||||
|
case Color::Black:
|
||||||
|
fillArc<Color::Black>(maxRadius, cx, cy, xDir, yDir);
|
||||||
|
break;
|
||||||
|
case Color::White:
|
||||||
|
fillArc<Color::White>(maxRadius, cx, cy, xDir, yDir);
|
||||||
|
break;
|
||||||
|
case Color::LightGray:
|
||||||
|
fillArc<Color::LightGray>(maxRadius, cx, cy, xDir, yDir);
|
||||||
|
break;
|
||||||
|
case Color::DarkGray:
|
||||||
|
fillArc<Color::DarkGray>(maxRadius, cx, cy, xDir, yDir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (roundTopLeft) {
|
||||||
|
fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color);
|
||||||
|
} else {
|
||||||
|
fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundTopRight) {
|
||||||
|
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color);
|
||||||
|
} else {
|
||||||
|
fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundBottomRight) {
|
||||||
|
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color);
|
||||||
|
} else {
|
||||||
|
fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundBottomLeft) {
|
||||||
|
fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color);
|
||||||
|
} else {
|
||||||
|
fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||||
int rotatedX = 0;
|
int rotatedX = 0;
|
||||||
int rotatedY = 0;
|
int rotatedY = 0;
|
||||||
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY);
|
||||||
// Rotate origin corner
|
// Rotate origin corner
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Portrait:
|
case Portrait:
|
||||||
@@ -164,7 +401,11 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// TODO: Rotate bits
|
// TODO: Rotate bits
|
||||||
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||||
|
display.drawImage(bitmap, y, getScreenWidth() - width - x, height, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
||||||
@@ -179,8 +420,8 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
bool isScaled = false;
|
bool isScaled = false;
|
||||||
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
||||||
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
||||||
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||||
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||||
|
|
||||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||||
@@ -190,7 +431,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||||
isScaled = true;
|
isScaled = true;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
||||||
|
|
||||||
// Calculate output row size (2 bits per pixel, packed into bytes)
|
// Calculate output row size (2 bits per pixel, packed into bytes)
|
||||||
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
||||||
@@ -199,7 +440,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||||
|
|
||||||
if (!outputRow || !rowBytes) {
|
if (!outputRow || !rowBytes) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
|
LOG_ERR("GFX", "!! Failed to allocate BMP row buffers");
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -218,7 +459,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
|
LOG_ERR("GFX", "Failed to read row %d from bitmap", bmpY);
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -281,7 +522,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||||
|
|
||||||
if (!outputRow || !rowBytes) {
|
if (!outputRow || !rowBytes) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate 1-bit BMP row buffers\n", millis());
|
LOG_ERR("GFX", "!! Failed to allocate 1-bit BMP row buffers");
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -290,7 +531,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||||
// Read rows sequentially using readNextRow
|
// Read rows sequentially using readNextRow
|
||||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [GFX] Failed to read row %d from 1-bit bitmap\n", millis(), bmpY);
|
LOG_ERR("GFX", "Failed to read row %d from 1-bit bitmap", bmpY);
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -348,7 +589,7 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
|
|||||||
// Allocate node buffer for scanline algorithm
|
// Allocate node buffer for scanline algorithm
|
||||||
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
||||||
if (!nodeX) {
|
if (!nodeX) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate polygon node buffer\n", millis());
|
LOG_ERR("GFX", "!! Failed to allocate polygon node buffer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,32 +640,43 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
|
|||||||
free(nodeX);
|
free(nodeX);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
|
// For performance measurement (using static to allow "const" methods)
|
||||||
|
static unsigned long start_ms = 0;
|
||||||
|
|
||||||
|
void GfxRenderer::clearScreen(const uint8_t color) const {
|
||||||
|
start_ms = millis();
|
||||||
|
display.clearScreen(color);
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::invertScreen() const {
|
void GfxRenderer::invertScreen() const {
|
||||||
uint8_t* buffer = einkDisplay.getFrameBuffer();
|
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
||||||
if (!buffer) {
|
frameBuffer[i] = ~frameBuffer[i];
|
||||||
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
|
||||||
buffer[i] = ~buffer[i];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
|
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
||||||
einkDisplay.displayBuffer(refreshMode);
|
auto elapsed = millis() - start_ms;
|
||||||
|
LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed);
|
||||||
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
|
if (!text || maxWidth <= 0) return "";
|
||||||
|
|
||||||
std::string item = text;
|
std::string item = text;
|
||||||
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
const char* ellipsis = "...";
|
||||||
while (itemWidth > maxWidth && item.length() > 8) {
|
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
||||||
item.replace(item.length() - 5, 5, "...");
|
if (textWidth <= maxWidth) {
|
||||||
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
// Text fits, return as is
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
|
while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) {
|
||||||
|
utf8RemoveLastChar(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.empty() ? ellipsis : item + ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
||||||
@@ -433,13 +685,13 @@ int GfxRenderer::getScreenWidth() const {
|
|||||||
case Portrait:
|
case Portrait:
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
// 480px wide in portrait logical coordinates
|
// 480px wide in portrait logical coordinates
|
||||||
return EInkDisplay::DISPLAY_HEIGHT;
|
return HalDisplay::DISPLAY_HEIGHT;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
// 800px wide in landscape logical coordinates
|
// 800px wide in landscape logical coordinates
|
||||||
return EInkDisplay::DISPLAY_WIDTH;
|
return HalDisplay::DISPLAY_WIDTH;
|
||||||
}
|
}
|
||||||
return EInkDisplay::DISPLAY_HEIGHT;
|
return HalDisplay::DISPLAY_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getScreenHeight() const {
|
int GfxRenderer::getScreenHeight() const {
|
||||||
@@ -447,27 +699,41 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
case Portrait:
|
case Portrait:
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
// 800px tall in portrait logical coordinates
|
// 800px tall in portrait logical coordinates
|
||||||
return EInkDisplay::DISPLAY_WIDTH;
|
return HalDisplay::DISPLAY_WIDTH;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
// 480px tall in landscape logical coordinates
|
// 480px tall in landscape logical coordinates
|
||||||
return EInkDisplay::DISPLAY_HEIGHT;
|
return HalDisplay::DISPLAY_HEIGHT;
|
||||||
}
|
}
|
||||||
return EInkDisplay::DISPLAY_WIDTH;
|
return HalDisplay::DISPLAY_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||||
|
if (fontMap.count(fontId) == 0) {
|
||||||
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cp;
|
||||||
|
int width = 0;
|
||||||
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
|
width += fontMap.at(fontId).getGlyph(cp, EpdFontFamily::REGULAR)->advanceX;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,95 +742,16 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|||||||
|
|
||||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
|
|
||||||
const char* btn4) {
|
|
||||||
const Orientation orig_orientation = getOrientation();
|
|
||||||
setOrientation(Orientation::Portrait);
|
|
||||||
|
|
||||||
const int pageHeight = getScreenHeight();
|
|
||||||
constexpr int buttonWidth = 106;
|
|
||||||
constexpr int buttonHeight = 40;
|
|
||||||
constexpr int buttonY = 40; // Distance from bottom
|
|
||||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
|
||||||
constexpr int buttonPositions[] = {25, 130, 245, 350};
|
|
||||||
const char* labels[] = {btn1, btn2, btn3, btn4};
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
// Only draw if the label is non-empty
|
|
||||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
|
||||||
const int x = buttonPositions[i];
|
|
||||||
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
|
|
||||||
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
|
|
||||||
const int textWidth = getTextWidth(fontId, labels[i]);
|
|
||||||
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
|
|
||||||
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOrientation(orig_orientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {
|
|
||||||
const int screenWidth = getScreenWidth();
|
|
||||||
constexpr int buttonWidth = 40; // Width on screen (height when rotated)
|
|
||||||
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
|
||||||
constexpr int buttonX = 5; // Distance from right edge
|
|
||||||
// Position for the button group - buttons share a border so they're adjacent
|
|
||||||
constexpr int topButtonY = 345; // Top button position
|
|
||||||
|
|
||||||
const char* labels[] = {topBtn, bottomBtn};
|
|
||||||
|
|
||||||
// Draw the shared border for both buttons as one unit
|
|
||||||
const int x = screenWidth - buttonX - buttonWidth;
|
|
||||||
|
|
||||||
// Draw top button outline (3 sides, bottom open)
|
|
||||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
|
||||||
drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY); // Top
|
|
||||||
drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1); // Left
|
|
||||||
drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1); // Right
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw shared middle border
|
|
||||||
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
|
||||||
drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight); // Shared border
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw bottom button outline (3 sides, top is shared)
|
|
||||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
|
||||||
drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1); // Left
|
|
||||||
drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
|
||||||
topButtonY + 2 * buttonHeight - 1); // Right
|
|
||||||
drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1); // Bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw text for each button
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
|
||||||
const int y = topButtonY + i * buttonHeight;
|
|
||||||
|
|
||||||
// Draw rotated text centered in the button
|
|
||||||
const int textWidth = getTextWidth(fontId, labels[i]);
|
|
||||||
const int textHeight = getTextHeight(fontId);
|
|
||||||
|
|
||||||
// Center the rotated text in the button
|
|
||||||
const int textX = x + (buttonWidth - textHeight) / 2;
|
|
||||||
const int textY = y + (buttonHeight + textWidth) / 2;
|
|
||||||
|
|
||||||
drawTextRotated90CW(fontId, textX, textY, labels[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int GfxRenderer::getTextHeight(const int fontId) const {
|
int GfxRenderer::getTextHeight(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||||
@@ -578,7 +765,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto font = fontMap.at(fontId);
|
const auto font = fontMap.at(fontId);
|
||||||
@@ -653,17 +840,18 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||||
|
|
||||||
size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; }
|
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||||
|
|
||||||
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
|
// unused
|
||||||
|
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
||||||
|
|
||||||
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
|
void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); }
|
||||||
|
|
||||||
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
|
void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); }
|
||||||
|
|
||||||
void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }
|
void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); }
|
||||||
|
|
||||||
void GfxRenderer::freeBwBufferChunks() {
|
void GfxRenderer::freeBwBufferChunks() {
|
||||||
for (auto& bwBufferChunk : bwBufferChunks) {
|
for (auto& bwBufferChunk : bwBufferChunks) {
|
||||||
@@ -681,18 +869,11 @@ void GfxRenderer::freeBwBufferChunks() {
|
|||||||
* Returns true if buffer was stored successfully, false if allocation failed.
|
* Returns true if buffer was stored successfully, false if allocation failed.
|
||||||
*/
|
*/
|
||||||
bool GfxRenderer::storeBwBuffer() {
|
bool GfxRenderer::storeBwBuffer() {
|
||||||
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
|
||||||
if (!frameBuffer) {
|
|
||||||
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate and copy each chunk
|
// Allocate and copy each chunk
|
||||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||||
// Check if any chunks are already allocated
|
// Check if any chunks are already allocated
|
||||||
if (bwBufferChunks[i]) {
|
if (bwBufferChunks[i]) {
|
||||||
Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n",
|
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
||||||
millis(), i);
|
|
||||||
free(bwBufferChunks[i]);
|
free(bwBufferChunks[i]);
|
||||||
bwBufferChunks[i] = nullptr;
|
bwBufferChunks[i] = nullptr;
|
||||||
}
|
}
|
||||||
@@ -701,8 +882,7 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
||||||
|
|
||||||
if (!bwBufferChunks[i]) {
|
if (!bwBufferChunks[i]) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i,
|
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, BW_BUFFER_CHUNK_SIZE);
|
||||||
BW_BUFFER_CHUNK_SIZE);
|
|
||||||
// Free previously allocated chunks
|
// Free previously allocated chunks
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
return false;
|
return false;
|
||||||
@@ -711,8 +891,7 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE);
|
||||||
BW_BUFFER_CHUNK_SIZE);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,17 +915,10 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
|
||||||
if (!frameBuffer) {
|
|
||||||
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
|
|
||||||
freeBwBufferChunks();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||||
// Check if chunk is missing
|
// Check if chunk is missing
|
||||||
if (!bwBufferChunks[i]) {
|
if (!bwBufferChunks[i]) {
|
||||||
Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis());
|
LOG_ERR("GFX", "!! BW buffer chunks not stored - this is likely a bug");
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -755,10 +927,10 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
|
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
|
LOG_DBG("GFX", "Restored and freed BW buffer chunks");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -766,9 +938,8 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
* Use this when BW buffer was re-rendered instead of stored/restored.
|
* Use this when BW buffer was re-rendered instead of stored/restored.
|
||||||
*/
|
*/
|
||||||
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
||||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
|
||||||
if (frameBuffer) {
|
if (frameBuffer) {
|
||||||
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,7 +952,7 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
|
|||||||
|
|
||||||
// no glyph?
|
// no glyph?
|
||||||
if (!glyph) {
|
if (!glyph) {
|
||||||
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
LOG_ERR("GFX", "No glyph for codepoint %d", cp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <EInkDisplay.h>
|
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
|
#include <HalDisplay.h>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "Bitmap.h"
|
#include "Bitmap.h"
|
||||||
|
|
||||||
|
// Color representation: uint8_t mapped to 4x4 Bayer matrix dithering levels
|
||||||
|
// 0 = transparent, 1-16 = gray levels (white to black)
|
||||||
|
enum Color : uint8_t { Clear = 0x00, White = 0x01, LightGray = 0x05, DarkGray = 0x0A, Black = 0x10 };
|
||||||
|
|
||||||
class GfxRenderer {
|
class GfxRenderer {
|
||||||
public:
|
public:
|
||||||
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||||
@@ -21,22 +25,28 @@ class GfxRenderer {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
||||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
|
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE,
|
||||||
"BW buffer chunking does not line up with display buffer size");
|
"BW buffer chunking does not line up with display buffer size");
|
||||||
|
|
||||||
EInkDisplay& einkDisplay;
|
HalDisplay& display;
|
||||||
RenderMode renderMode;
|
RenderMode renderMode;
|
||||||
Orientation orientation;
|
Orientation orientation;
|
||||||
|
bool fadingFix;
|
||||||
|
uint8_t* frameBuffer = nullptr;
|
||||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||||
std::map<int, EpdFontFamily> fontMap;
|
std::map<int, EpdFontFamily> fontMap;
|
||||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||||
EpdFontFamily::Style style) const;
|
EpdFontFamily::Style style) const;
|
||||||
void freeBwBufferChunks();
|
void freeBwBufferChunks();
|
||||||
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
|
template <Color color>
|
||||||
|
void drawPixelDither(int x, int y) const;
|
||||||
|
template <Color color>
|
||||||
|
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
explicit GfxRenderer(HalDisplay& halDisplay)
|
||||||
|
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
|
||||||
~GfxRenderer() { freeBwBufferChunks(); }
|
~GfxRenderer() { freeBwBufferChunks(); }
|
||||||
|
|
||||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
||||||
@@ -45,27 +55,43 @@ class GfxRenderer {
|
|||||||
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
|
void begin(); // must be called right after display.begin()
|
||||||
void insertFont(int fontId, EpdFontFamily font);
|
void insertFont(int fontId, EpdFontFamily font);
|
||||||
|
|
||||||
// Orientation control (affects logical width/height and coordinate transforms)
|
// Orientation control (affects logical width/height and coordinate transforms)
|
||||||
void setOrientation(const Orientation o) { orientation = o; }
|
void setOrientation(const Orientation o) { orientation = o; }
|
||||||
Orientation getOrientation() const { return orientation; }
|
Orientation getOrientation() const { return orientation; }
|
||||||
|
|
||||||
|
// Fading fix control
|
||||||
|
void setFadingFix(const bool enabled) { fadingFix = enabled; }
|
||||||
|
|
||||||
// Screen ops
|
// Screen ops
|
||||||
int getScreenWidth() const;
|
int getScreenWidth() const;
|
||||||
int getScreenHeight() const;
|
int getScreenHeight() const;
|
||||||
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||||
void displayWindow(int x, int y, int width, int height) const;
|
// void displayWindow(int x, int y, int width, int height) const;
|
||||||
void invertScreen() const;
|
void invertScreen() const;
|
||||||
void clearScreen(uint8_t color = 0xFF) const;
|
void clearScreen(uint8_t color = 0xFF) const;
|
||||||
|
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||||
|
|
||||||
// Drawing
|
// Drawing
|
||||||
void drawPixel(int x, int y, bool state = true) const;
|
void drawPixel(int x, int y, bool state = true) const;
|
||||||
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||||
|
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
|
||||||
|
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
||||||
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
|
void drawRect(int x, int y, int width, int height, int lineWidth, bool state) const;
|
||||||
|
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
|
||||||
|
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool roundTopLeft,
|
||||||
|
bool roundTopRight, bool roundBottomLeft, bool roundBottomRight, bool state) const;
|
||||||
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
|
void fillRectDither(int x, int y, int width, int height, Color color) const;
|
||||||
|
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, Color color) const;
|
||||||
|
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
|
||||||
|
bool roundBottomLeft, bool roundBottomRight, Color color) const;
|
||||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
|
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||||
float cropY = 0) const;
|
float cropY = 0) const;
|
||||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||||
@@ -78,24 +104,20 @@ class GfxRenderer {
|
|||||||
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
||||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||||
int getSpaceWidth(int fontId) const;
|
int getSpaceWidth(int fontId) const;
|
||||||
|
int getTextAdvanceX(int fontId, const char* text) const;
|
||||||
int getFontAscenderSize(int fontId) const;
|
int getFontAscenderSize(int fontId) const;
|
||||||
int getLineHeight(int fontId) const;
|
int getLineHeight(int fontId) const;
|
||||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||||
|
|
||||||
// UI Components
|
|
||||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
|
|
||||||
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
|
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
|
||||||
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
||||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||||
int getTextHeight(int fontId) const;
|
int getTextHeight(int fontId) const;
|
||||||
|
|
||||||
public:
|
|
||||||
// Grayscale functions
|
// Grayscale functions
|
||||||
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
||||||
|
RenderMode getRenderMode() const { return renderMode; }
|
||||||
void copyGrayscaleLsbBuffers() const;
|
void copyGrayscaleLsbBuffers() const;
|
||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() const;
|
void displayGrayBuffer() const;
|
||||||
@@ -106,6 +128,4 @@ class GfxRenderer {
|
|||||||
// Low level functions
|
// Low level functions
|
||||||
uint8_t* getFrameBuffer() const;
|
uint8_t* getFrameBuffer() const;
|
||||||
static size_t getBufferSize();
|
static size_t getBufferSize();
|
||||||
void grayscaleRevert() const;
|
|
||||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
|
||||||
};
|
};
|
||||||
|
|||||||
96
lib/I18n/I18n.cpp
Normal file
96
lib/I18n/I18n.cpp
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#include "I18n.h"
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include "I18nStrings.h"
|
||||||
|
|
||||||
|
using namespace i18n_strings;
|
||||||
|
|
||||||
|
// Settings file path
|
||||||
|
static constexpr const char* SETTINGS_FILE = "/.crosspoint/language.bin";
|
||||||
|
static constexpr uint8_t SETTINGS_VERSION = 1;
|
||||||
|
|
||||||
|
I18n& I18n::getInstance() {
|
||||||
|
static I18n instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* I18n::get(StrId id) const {
|
||||||
|
const auto index = static_cast<size_t>(id);
|
||||||
|
if (index >= static_cast<size_t>(StrId::_COUNT)) {
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generated helper function - no hardcoded switch needed!
|
||||||
|
const char* const* strings = getStringArray(_language);
|
||||||
|
return strings[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void I18n::setLanguage(Language lang) {
|
||||||
|
if (lang >= Language::_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_language = lang;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* I18n::getLanguageName(Language lang) const {
|
||||||
|
const auto index = static_cast<size_t>(lang);
|
||||||
|
if (index >= static_cast<size_t>(Language::_COUNT)) {
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
return LANGUAGE_NAMES[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void I18n::saveSettings() {
|
||||||
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForWrite("I18N", SETTINGS_FILE, file)) {
|
||||||
|
Serial.printf("[I18N] Failed to save settings\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::writePod(file, SETTINGS_VERSION);
|
||||||
|
serialization::writePod(file, static_cast<uint8_t>(_language));
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
Serial.printf("[I18N] Settings saved: language=%d\n", static_cast<int>(_language));
|
||||||
|
}
|
||||||
|
|
||||||
|
void I18n::loadSettings() {
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("I18N", SETTINGS_FILE, file)) {
|
||||||
|
Serial.printf("[I18N] No settings file, using default (English)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(file, version);
|
||||||
|
if (version != SETTINGS_VERSION) {
|
||||||
|
Serial.printf("[I18N] Settings version mismatch\n");
|
||||||
|
file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t lang;
|
||||||
|
serialization::readPod(file, lang);
|
||||||
|
if (lang < static_cast<size_t>(Language::_COUNT)) {
|
||||||
|
_language = static_cast<Language>(lang);
|
||||||
|
Serial.printf("[I18N] Loaded language: %d\n", static_cast<int>(_language));
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate character set for a specific language
|
||||||
|
const char* I18n::getCharacterSet(Language lang) {
|
||||||
|
const auto langIndex = static_cast<size_t>(lang);
|
||||||
|
if (langIndex >= static_cast<size_t>(Language::_COUNT)) {
|
||||||
|
lang = Language::ENGLISH; // Fallback to first language
|
||||||
|
}
|
||||||
|
|
||||||
|
return CHARACTER_SETS[static_cast<size_t>(lang)];
|
||||||
|
}
|
||||||
42
lib/I18n/I18n.h
Normal file
42
lib/I18n/I18n.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "I18nKeys.h"
|
||||||
|
/**
|
||||||
|
* Internationalization (i18n) system for CrossPoint Reader
|
||||||
|
*/
|
||||||
|
|
||||||
|
class I18n {
|
||||||
|
public:
|
||||||
|
static I18n& getInstance();
|
||||||
|
|
||||||
|
// Disable copy
|
||||||
|
I18n(const I18n&) = delete;
|
||||||
|
I18n& operator=(const I18n&) = delete;
|
||||||
|
|
||||||
|
// Get localized string by ID
|
||||||
|
const char* get(StrId id) const;
|
||||||
|
|
||||||
|
const char* operator[](StrId id) const { return get(id); }
|
||||||
|
|
||||||
|
Language getLanguage() const { return _language; }
|
||||||
|
void setLanguage(Language lang);
|
||||||
|
const char* getLanguageName(Language lang) const;
|
||||||
|
|
||||||
|
void saveSettings();
|
||||||
|
void loadSettings();
|
||||||
|
|
||||||
|
// Get all unique characters used in a specific language
|
||||||
|
// Returns a sorted string of unique characters
|
||||||
|
static const char* getCharacterSet(Language lang);
|
||||||
|
|
||||||
|
private:
|
||||||
|
I18n() : _language(Language::ENGLISH) {}
|
||||||
|
|
||||||
|
Language _language;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience macros
|
||||||
|
#define tr(id) I18n::getInstance().get(StrId::id)
|
||||||
|
#define I18N I18n::getInstance()
|
||||||
381
lib/I18n/I18nKeys.h
Normal file
381
lib/I18n/I18nKeys.h
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Forward declaration for string arrays
|
||||||
|
namespace i18n_strings {
|
||||||
|
extern const char* const STRINGS_EN[];
|
||||||
|
extern const char* const STRINGS_ES[];
|
||||||
|
extern const char* const STRINGS_FR[];
|
||||||
|
extern const char* const STRINGS_DE[];
|
||||||
|
extern const char* const STRINGS_CZ[];
|
||||||
|
extern const char* const STRINGS_PO[];
|
||||||
|
extern const char* const STRINGS_RU[];
|
||||||
|
extern const char* const STRINGS_SV[];
|
||||||
|
} // namespace i18n_strings
|
||||||
|
|
||||||
|
// Language enum
|
||||||
|
enum class Language : uint8_t {
|
||||||
|
ENGLISH = 0,
|
||||||
|
SPANISH = 1,
|
||||||
|
FRENCH = 2,
|
||||||
|
GERMAN = 3,
|
||||||
|
CZECH = 4,
|
||||||
|
PORTUGUESE = 5,
|
||||||
|
RUSSIAN = 6,
|
||||||
|
SWEDISH = 7,
|
||||||
|
_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
// Language display names (defined in I18nStrings.cpp)
|
||||||
|
extern const char* const LANGUAGE_NAMES[];
|
||||||
|
|
||||||
|
// Character sets for each language (defined in I18nStrings.cpp)
|
||||||
|
extern const char* const CHARACTER_SETS[];
|
||||||
|
|
||||||
|
// String IDs
|
||||||
|
enum class StrId : uint16_t {
|
||||||
|
STR_CROSSPOINT,
|
||||||
|
STR_BOOTING,
|
||||||
|
STR_SLEEPING,
|
||||||
|
STR_ENTERING_SLEEP,
|
||||||
|
STR_BROWSE_FILES,
|
||||||
|
STR_FILE_TRANSFER,
|
||||||
|
STR_SETTINGS_TITLE,
|
||||||
|
STR_CALIBRE_LIBRARY,
|
||||||
|
STR_CONTINUE_READING,
|
||||||
|
STR_NO_OPEN_BOOK,
|
||||||
|
STR_START_READING,
|
||||||
|
STR_BOOKS,
|
||||||
|
STR_NO_BOOKS_FOUND,
|
||||||
|
STR_SELECT_CHAPTER,
|
||||||
|
STR_NO_CHAPTERS,
|
||||||
|
STR_END_OF_BOOK,
|
||||||
|
STR_EMPTY_CHAPTER,
|
||||||
|
STR_INDEXING,
|
||||||
|
STR_MEMORY_ERROR,
|
||||||
|
STR_PAGE_LOAD_ERROR,
|
||||||
|
STR_EMPTY_FILE,
|
||||||
|
STR_OUT_OF_BOUNDS,
|
||||||
|
STR_LOADING,
|
||||||
|
STR_LOAD_XTC_FAILED,
|
||||||
|
STR_LOAD_TXT_FAILED,
|
||||||
|
STR_LOAD_EPUB_FAILED,
|
||||||
|
STR_SD_CARD_ERROR,
|
||||||
|
STR_WIFI_NETWORKS,
|
||||||
|
STR_NO_NETWORKS,
|
||||||
|
STR_NETWORKS_FOUND,
|
||||||
|
STR_SCANNING,
|
||||||
|
STR_CONNECTING,
|
||||||
|
STR_CONNECTED,
|
||||||
|
STR_CONNECTION_FAILED,
|
||||||
|
STR_CONNECTION_TIMEOUT,
|
||||||
|
STR_FORGET_NETWORK,
|
||||||
|
STR_SAVE_PASSWORD,
|
||||||
|
STR_REMOVE_PASSWORD,
|
||||||
|
STR_PRESS_OK_SCAN,
|
||||||
|
STR_PRESS_ANY_CONTINUE,
|
||||||
|
STR_SELECT_HINT,
|
||||||
|
STR_HOW_CONNECT,
|
||||||
|
STR_JOIN_NETWORK,
|
||||||
|
STR_CREATE_HOTSPOT,
|
||||||
|
STR_JOIN_DESC,
|
||||||
|
STR_HOTSPOT_DESC,
|
||||||
|
STR_STARTING_HOTSPOT,
|
||||||
|
STR_HOTSPOT_MODE,
|
||||||
|
STR_CONNECT_WIFI_HINT,
|
||||||
|
STR_OPEN_URL_HINT,
|
||||||
|
STR_OR_HTTP_PREFIX,
|
||||||
|
STR_SCAN_QR_HINT,
|
||||||
|
STR_CALIBRE_WIRELESS,
|
||||||
|
STR_CALIBRE_WEB_URL,
|
||||||
|
STR_CONNECT_WIRELESS,
|
||||||
|
STR_NETWORK_LEGEND,
|
||||||
|
STR_MAC_ADDRESS,
|
||||||
|
STR_CHECKING_WIFI,
|
||||||
|
STR_ENTER_WIFI_PASSWORD,
|
||||||
|
STR_ENTER_TEXT,
|
||||||
|
STR_TO_PREFIX,
|
||||||
|
STR_CALIBRE_DISCOVERING,
|
||||||
|
STR_CALIBRE_CONNECTING_TO,
|
||||||
|
STR_CALIBRE_CONNECTED_TO,
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS,
|
||||||
|
STR_CONNECTION_FAILED_RETRYING,
|
||||||
|
STR_CALIBRE_DISCONNECTED,
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER,
|
||||||
|
STR_CALIBRE_TRANSFER_HINT,
|
||||||
|
STR_CALIBRE_RECEIVING,
|
||||||
|
STR_CALIBRE_RECEIVED,
|
||||||
|
STR_CALIBRE_WAITING_MORE,
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE,
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED,
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED,
|
||||||
|
STR_CALIBRE_INSTRUCTION_1,
|
||||||
|
STR_CALIBRE_INSTRUCTION_2,
|
||||||
|
STR_CALIBRE_INSTRUCTION_3,
|
||||||
|
STR_CALIBRE_INSTRUCTION_4,
|
||||||
|
STR_CAT_DISPLAY,
|
||||||
|
STR_CAT_READER,
|
||||||
|
STR_CAT_CONTROLS,
|
||||||
|
STR_CAT_SYSTEM,
|
||||||
|
STR_SLEEP_SCREEN,
|
||||||
|
STR_SLEEP_COVER_MODE,
|
||||||
|
STR_STATUS_BAR,
|
||||||
|
STR_HIDE_BATTERY,
|
||||||
|
STR_EXTRA_SPACING,
|
||||||
|
STR_TEXT_AA,
|
||||||
|
STR_SHORT_PWR_BTN,
|
||||||
|
STR_ORIENTATION,
|
||||||
|
STR_FRONT_BTN_LAYOUT,
|
||||||
|
STR_SIDE_BTN_LAYOUT,
|
||||||
|
STR_LONG_PRESS_SKIP,
|
||||||
|
STR_FONT_FAMILY,
|
||||||
|
STR_EXT_READER_FONT,
|
||||||
|
STR_EXT_CHINESE_FONT,
|
||||||
|
STR_EXT_UI_FONT,
|
||||||
|
STR_FONT_SIZE,
|
||||||
|
STR_LINE_SPACING,
|
||||||
|
STR_ASCII_LETTER_SPACING,
|
||||||
|
STR_ASCII_DIGIT_SPACING,
|
||||||
|
STR_CJK_SPACING,
|
||||||
|
STR_COLOR_MODE,
|
||||||
|
STR_SCREEN_MARGIN,
|
||||||
|
STR_PARA_ALIGNMENT,
|
||||||
|
STR_HYPHENATION,
|
||||||
|
STR_TIME_TO_SLEEP,
|
||||||
|
STR_REFRESH_FREQ,
|
||||||
|
STR_CALIBRE_SETTINGS,
|
||||||
|
STR_KOREADER_SYNC,
|
||||||
|
STR_CHECK_UPDATES,
|
||||||
|
STR_LANGUAGE,
|
||||||
|
STR_SELECT_WALLPAPER,
|
||||||
|
STR_CLEAR_READING_CACHE,
|
||||||
|
STR_CALIBRE,
|
||||||
|
STR_USERNAME,
|
||||||
|
STR_PASSWORD,
|
||||||
|
STR_SYNC_SERVER_URL,
|
||||||
|
STR_DOCUMENT_MATCHING,
|
||||||
|
STR_AUTHENTICATE,
|
||||||
|
STR_KOREADER_USERNAME,
|
||||||
|
STR_KOREADER_PASSWORD,
|
||||||
|
STR_FILENAME,
|
||||||
|
STR_BINARY,
|
||||||
|
STR_SET_CREDENTIALS_FIRST,
|
||||||
|
STR_WIFI_CONN_FAILED,
|
||||||
|
STR_AUTHENTICATING,
|
||||||
|
STR_AUTH_SUCCESS,
|
||||||
|
STR_KOREADER_AUTH,
|
||||||
|
STR_SYNC_READY,
|
||||||
|
STR_AUTH_FAILED,
|
||||||
|
STR_DONE,
|
||||||
|
STR_CLEAR_CACHE_WARNING_1,
|
||||||
|
STR_CLEAR_CACHE_WARNING_2,
|
||||||
|
STR_CLEAR_CACHE_WARNING_3,
|
||||||
|
STR_CLEAR_CACHE_WARNING_4,
|
||||||
|
STR_CLEARING_CACHE,
|
||||||
|
STR_CACHE_CLEARED,
|
||||||
|
STR_ITEMS_REMOVED,
|
||||||
|
STR_FAILED_LOWER,
|
||||||
|
STR_CLEAR_CACHE_FAILED,
|
||||||
|
STR_CHECK_SERIAL_OUTPUT,
|
||||||
|
STR_DARK,
|
||||||
|
STR_LIGHT,
|
||||||
|
STR_CUSTOM,
|
||||||
|
STR_COVER,
|
||||||
|
STR_NONE_OPT,
|
||||||
|
STR_FIT,
|
||||||
|
STR_CROP,
|
||||||
|
STR_NO_PROGRESS,
|
||||||
|
STR_FULL_OPT,
|
||||||
|
STR_NEVER,
|
||||||
|
STR_IN_READER,
|
||||||
|
STR_ALWAYS,
|
||||||
|
STR_IGNORE,
|
||||||
|
STR_SLEEP,
|
||||||
|
STR_PAGE_TURN,
|
||||||
|
STR_PORTRAIT,
|
||||||
|
STR_LANDSCAPE_CW,
|
||||||
|
STR_INVERTED,
|
||||||
|
STR_LANDSCAPE_CCW,
|
||||||
|
STR_FRONT_LAYOUT_BCLR,
|
||||||
|
STR_FRONT_LAYOUT_LRBC,
|
||||||
|
STR_FRONT_LAYOUT_LBCR,
|
||||||
|
STR_PREV_NEXT,
|
||||||
|
STR_NEXT_PREV,
|
||||||
|
STR_BOOKERLY,
|
||||||
|
STR_NOTO_SANS,
|
||||||
|
STR_OPEN_DYSLEXIC,
|
||||||
|
STR_SMALL,
|
||||||
|
STR_MEDIUM,
|
||||||
|
STR_LARGE,
|
||||||
|
STR_X_LARGE,
|
||||||
|
STR_TIGHT,
|
||||||
|
STR_NORMAL,
|
||||||
|
STR_WIDE,
|
||||||
|
STR_JUSTIFY,
|
||||||
|
STR_ALIGN_LEFT,
|
||||||
|
STR_CENTER,
|
||||||
|
STR_ALIGN_RIGHT,
|
||||||
|
STR_MIN_1,
|
||||||
|
STR_MIN_5,
|
||||||
|
STR_MIN_10,
|
||||||
|
STR_MIN_15,
|
||||||
|
STR_MIN_30,
|
||||||
|
STR_PAGES_1,
|
||||||
|
STR_PAGES_5,
|
||||||
|
STR_PAGES_10,
|
||||||
|
STR_PAGES_15,
|
||||||
|
STR_PAGES_30,
|
||||||
|
STR_UPDATE,
|
||||||
|
STR_CHECKING_UPDATE,
|
||||||
|
STR_NEW_UPDATE,
|
||||||
|
STR_CURRENT_VERSION,
|
||||||
|
STR_NEW_VERSION,
|
||||||
|
STR_UPDATING,
|
||||||
|
STR_NO_UPDATE,
|
||||||
|
STR_UPDATE_FAILED,
|
||||||
|
STR_UPDATE_COMPLETE,
|
||||||
|
STR_POWER_ON_HINT,
|
||||||
|
STR_EXTERNAL_FONT,
|
||||||
|
STR_BUILTIN_DISABLED,
|
||||||
|
STR_NO_ENTRIES,
|
||||||
|
STR_DOWNLOADING,
|
||||||
|
STR_DOWNLOAD_FAILED,
|
||||||
|
STR_ERROR_MSG,
|
||||||
|
STR_UNNAMED,
|
||||||
|
STR_NO_SERVER_URL,
|
||||||
|
STR_FETCH_FEED_FAILED,
|
||||||
|
STR_PARSE_FEED_FAILED,
|
||||||
|
STR_NETWORK_PREFIX,
|
||||||
|
STR_IP_ADDRESS_PREFIX,
|
||||||
|
STR_SCAN_QR_WIFI_HINT,
|
||||||
|
STR_ERROR_GENERAL_FAILURE,
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND,
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT,
|
||||||
|
STR_SD_CARD,
|
||||||
|
STR_BACK,
|
||||||
|
STR_EXIT,
|
||||||
|
STR_HOME,
|
||||||
|
STR_SAVE,
|
||||||
|
STR_SELECT,
|
||||||
|
STR_TOGGLE,
|
||||||
|
STR_CONFIRM,
|
||||||
|
STR_CANCEL,
|
||||||
|
STR_CONNECT,
|
||||||
|
STR_OPEN,
|
||||||
|
STR_DOWNLOAD,
|
||||||
|
STR_RETRY,
|
||||||
|
STR_YES,
|
||||||
|
STR_NO,
|
||||||
|
STR_STATE_ON,
|
||||||
|
STR_STATE_OFF,
|
||||||
|
STR_SET,
|
||||||
|
STR_NOT_SET,
|
||||||
|
STR_DIR_LEFT,
|
||||||
|
STR_DIR_RIGHT,
|
||||||
|
STR_DIR_UP,
|
||||||
|
STR_DIR_DOWN,
|
||||||
|
STR_CAPS_ON,
|
||||||
|
STR_CAPS_OFF,
|
||||||
|
STR_OK_BUTTON,
|
||||||
|
STR_ON_MARKER,
|
||||||
|
STR_SLEEP_COVER_FILTER,
|
||||||
|
STR_FILTER_CONTRAST,
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT,
|
||||||
|
STR_STATUS_BAR_FULL_BOOK,
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY,
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER,
|
||||||
|
STR_UI_THEME,
|
||||||
|
STR_THEME_CLASSIC,
|
||||||
|
STR_THEME_LYRA,
|
||||||
|
STR_SUNLIGHT_FADING_FIX,
|
||||||
|
STR_REMAP_FRONT_BUTTONS,
|
||||||
|
STR_OPDS_BROWSER,
|
||||||
|
STR_COVER_CUSTOM,
|
||||||
|
STR_RECENTS,
|
||||||
|
STR_MENU_RECENT_BOOKS,
|
||||||
|
STR_NO_RECENT_BOOKS,
|
||||||
|
STR_CALIBRE_DESC,
|
||||||
|
STR_FORGET_AND_REMOVE,
|
||||||
|
STR_FORGET_BUTTON,
|
||||||
|
STR_CALIBRE_STARTING,
|
||||||
|
STR_CALIBRE_SETUP,
|
||||||
|
STR_CALIBRE_STATUS,
|
||||||
|
STR_CLEAR_BUTTON,
|
||||||
|
STR_DEFAULT_VALUE,
|
||||||
|
STR_REMAP_PROMPT,
|
||||||
|
STR_UNASSIGNED,
|
||||||
|
STR_ALREADY_ASSIGNED,
|
||||||
|
STR_REMAP_RESET_HINT,
|
||||||
|
STR_REMAP_CANCEL_HINT,
|
||||||
|
STR_HW_BACK_LABEL,
|
||||||
|
STR_HW_CONFIRM_LABEL,
|
||||||
|
STR_HW_LEFT_LABEL,
|
||||||
|
STR_HW_RIGHT_LABEL,
|
||||||
|
STR_GO_TO_PERCENT,
|
||||||
|
STR_GO_HOME_BUTTON,
|
||||||
|
STR_SYNC_PROGRESS,
|
||||||
|
STR_DELETE_CACHE,
|
||||||
|
STR_CHAPTER_PREFIX,
|
||||||
|
STR_PAGES_SEPARATOR,
|
||||||
|
STR_BOOK_PREFIX,
|
||||||
|
STR_KBD_SHIFT,
|
||||||
|
STR_KBD_SHIFT_CAPS,
|
||||||
|
STR_KBD_LOCK,
|
||||||
|
STR_CALIBRE_URL_HINT,
|
||||||
|
STR_PERCENT_STEP_HINT,
|
||||||
|
STR_SYNCING_TIME,
|
||||||
|
STR_CALC_HASH,
|
||||||
|
STR_HASH_FAILED,
|
||||||
|
STR_FETCH_PROGRESS,
|
||||||
|
STR_UPLOAD_PROGRESS,
|
||||||
|
STR_NO_CREDENTIALS_MSG,
|
||||||
|
STR_KOREADER_SETUP_HINT,
|
||||||
|
STR_PROGRESS_FOUND,
|
||||||
|
STR_REMOTE_LABEL,
|
||||||
|
STR_LOCAL_LABEL,
|
||||||
|
STR_PAGE_OVERALL_FORMAT,
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT,
|
||||||
|
STR_DEVICE_FROM_FORMAT,
|
||||||
|
STR_APPLY_REMOTE,
|
||||||
|
STR_UPLOAD_LOCAL,
|
||||||
|
STR_NO_REMOTE_MSG,
|
||||||
|
STR_UPLOAD_PROMPT,
|
||||||
|
STR_UPLOAD_SUCCESS,
|
||||||
|
STR_SYNC_FAILED_MSG,
|
||||||
|
STR_SECTION_PREFIX,
|
||||||
|
STR_UPLOAD,
|
||||||
|
STR_BOOK_S_STYLE,
|
||||||
|
STR_EMBEDDED_STYLE,
|
||||||
|
STR_OPDS_SERVER_URL,
|
||||||
|
// Sentinel - must be last
|
||||||
|
_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get string array for a language
|
||||||
|
inline const char* const* getStringArray(Language lang) {
|
||||||
|
switch (lang) {
|
||||||
|
case Language::ENGLISH:
|
||||||
|
return i18n_strings::STRINGS_EN;
|
||||||
|
case Language::SPANISH:
|
||||||
|
return i18n_strings::STRINGS_ES;
|
||||||
|
case Language::FRENCH:
|
||||||
|
return i18n_strings::STRINGS_FR;
|
||||||
|
case Language::GERMAN:
|
||||||
|
return i18n_strings::STRINGS_DE;
|
||||||
|
case Language::CZECH:
|
||||||
|
return i18n_strings::STRINGS_CZ;
|
||||||
|
case Language::PORTUGUESE:
|
||||||
|
return i18n_strings::STRINGS_PO;
|
||||||
|
case Language::RUSSIAN:
|
||||||
|
return i18n_strings::STRINGS_RU;
|
||||||
|
case Language::SWEDISH:
|
||||||
|
return i18n_strings::STRINGS_SV;
|
||||||
|
default:
|
||||||
|
return i18n_strings::STRINGS_EN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get language count
|
||||||
|
constexpr uint8_t getLanguageCount() { return static_cast<uint8_t>(Language::_COUNT); }
|
||||||
19
lib/I18n/I18nStrings.h
Normal file
19
lib/I18n/I18nStrings.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "I18nKeys.h"
|
||||||
|
|
||||||
|
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
|
||||||
|
|
||||||
|
namespace i18n_strings {
|
||||||
|
|
||||||
|
extern const char* const STRINGS_EN[];
|
||||||
|
extern const char* const STRINGS_ES[];
|
||||||
|
extern const char* const STRINGS_FR[];
|
||||||
|
extern const char* const STRINGS_DE[];
|
||||||
|
extern const char* const STRINGS_CZ[];
|
||||||
|
extern const char* const STRINGS_PO[];
|
||||||
|
extern const char* const STRINGS_RU[];
|
||||||
|
extern const char* const STRINGS_SV[];
|
||||||
|
|
||||||
|
} // namespace i18n_strings
|
||||||
317
lib/I18n/translations/czech.yaml
Normal file
317
lib/I18n/translations/czech.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Čeština"
|
||||||
|
_language_code: "CZECH"
|
||||||
|
_order: "4"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "SPUŠTĚNÍ"
|
||||||
|
STR_SLEEPING: "SPÁNEK"
|
||||||
|
STR_ENTERING_SLEEP: "Vstup do režimu spánku..."
|
||||||
|
STR_BROWSE_FILES: "Procházet soubory"
|
||||||
|
STR_FILE_TRANSFER: "Přenos souborů"
|
||||||
|
STR_SETTINGS_TITLE: "Nastavení"
|
||||||
|
STR_CALIBRE_LIBRARY: "Knihovna Calibre"
|
||||||
|
STR_CONTINUE_READING: "Pokračovat ve čtení"
|
||||||
|
STR_NO_OPEN_BOOK: "Žádná otevřená kniha"
|
||||||
|
STR_START_READING: "Začněte číst níže"
|
||||||
|
STR_BOOKS: "Knihy"
|
||||||
|
STR_NO_BOOKS_FOUND: "Žádné knihy nenalezeny"
|
||||||
|
STR_SELECT_CHAPTER: "Vybrat kapitolu"
|
||||||
|
STR_NO_CHAPTERS: "Žádné kapitoly"
|
||||||
|
STR_END_OF_BOOK: "Konec knihy"
|
||||||
|
STR_EMPTY_CHAPTER: "Prázdná kapitola"
|
||||||
|
STR_INDEXING: "Indexování..."
|
||||||
|
STR_MEMORY_ERROR: "Chyba paměti"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Chyba načítání stránky"
|
||||||
|
STR_EMPTY_FILE: "Prázdný soubor"
|
||||||
|
STR_OUT_OF_BOUNDS: "Mimo hranice"
|
||||||
|
STR_LOADING: "Načítání..."
|
||||||
|
STR_LOAD_XTC_FAILED: "Nepodařilo se načíst XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Nepodařilo se načíst TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Nepodařilo se načíst EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "Chyba SD karty"
|
||||||
|
STR_WIFI_NETWORKS: "Wi-Fi sítě"
|
||||||
|
STR_NO_NETWORKS: "Žádné sítě nenalezeny"
|
||||||
|
STR_NETWORKS_FOUND: "Nalezeno %zu sítí"
|
||||||
|
STR_SCANNING: "Skenování..."
|
||||||
|
STR_CONNECTING: "Připojování..."
|
||||||
|
STR_CONNECTED: "Připojeno!"
|
||||||
|
STR_CONNECTION_FAILED: "Připojení se nezdařilo"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Časový limit připojení"
|
||||||
|
STR_FORGET_NETWORK: "Zapomenout síť?"
|
||||||
|
STR_SAVE_PASSWORD: "Uložit heslo pro příště?"
|
||||||
|
STR_REMOVE_PASSWORD: "Odstranit uložené heslo?"
|
||||||
|
STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Pokračujte stiskem libovolné klávesy"
|
||||||
|
STR_SELECT_HINT: "VLEVO/VPRAVO: Vybrat | OK: Potvrdit"
|
||||||
|
STR_HOW_CONNECT: "Jak se chcete připojit?"
|
||||||
|
STR_JOIN_NETWORK: "Připojit se k síti"
|
||||||
|
STR_CREATE_HOTSPOT: "Vytvořit hotspot"
|
||||||
|
STR_JOIN_DESC: "Připojit se k existující síti WiFi"
|
||||||
|
STR_HOTSPOT_DESC: "Vytvořit síť WiFi, ke které se mohou připojit ostatní"
|
||||||
|
STR_STARTING_HOTSPOT: "Spouštění hotspotu..."
|
||||||
|
STR_HOTSPOT_MODE: "Režim hotspotu"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Připojte své zařízení k této síti WiFi"
|
||||||
|
STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
|
||||||
|
STR_OR_HTTP_PREFIX: "nebo http://"
|
||||||
|
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
|
STR_CALIBRE_WEB_URL: "URL webu Calibre"
|
||||||
|
STR_CONNECT_WIRELESS: "Připojit jako bezdrátové zařízení"
|
||||||
|
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
||||||
|
STR_MAC_ADDRESS: "MAC adresa:"
|
||||||
|
STR_CHECKING_WIFI: "Kontrola WiFi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi"
|
||||||
|
STR_ENTER_TEXT: "Zadejte text"
|
||||||
|
STR_TO_PREFIX: "pro"
|
||||||
|
STR_CALIBRE_DISCOVERING: "Prozkoumávání Calibre..."
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Připojování k"
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Připojeno k"
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Čekám na příkazy…"
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Připojení se nezdařilo, opakování pokusu)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre odpojeno"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Čekání na přenos..."
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Nezdaří-li se přenos, povolte\\n„Ignorovat volné místo“ v Calibre\\nnastavení pluginu SmartDevice."
|
||||||
|
STR_CALIBRE_RECEIVING: "Příjem:"
|
||||||
|
STR_CALIBRE_RECEIVED: "Přijato:"
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Čekání na další..."
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Nepodařilo se vytvořit soubor"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Vyžadováno heslo"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Přenos přerušen"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
|
||||||
|
STR_CAT_DISPLAY: "Displej"
|
||||||
|
STR_CAT_READER: "Čtečka"
|
||||||
|
STR_CAT_CONTROLS: "Ovládací prvky"
|
||||||
|
STR_CAT_SYSTEM: "Systém"
|
||||||
|
STR_SLEEP_SCREEN: "Obrazovka spánku"
|
||||||
|
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
|
||||||
|
STR_STATUS_BAR: "Stavový řádek"
|
||||||
|
STR_HIDE_BATTERY: "Skrýt baterii %"
|
||||||
|
STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
|
||||||
|
STR_TEXT_AA: "Vyhlazování textu"
|
||||||
|
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
|
||||||
|
STR_ORIENTATION: "Orientace čtení"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Rozvržení předních tlačítek"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
|
||||||
|
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
|
||||||
|
STR_FONT_FAMILY: "Rodina písem čtečky"
|
||||||
|
STR_EXT_READER_FONT: "Písmo externí čtečky"
|
||||||
|
STR_EXT_CHINESE_FONT: "Písmo čtečky"
|
||||||
|
STR_EXT_UI_FONT: "Písmo rozhraní"
|
||||||
|
STR_FONT_SIZE: "Velikost písma rozhraní"
|
||||||
|
STR_LINE_SPACING: "Řádkování čtečky"
|
||||||
|
STR_ASCII_LETTER_SPACING: "Mezery písmen ASCII"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "Mezery číslic ASCII"
|
||||||
|
STR_CJK_SPACING: "Mezery CJK"
|
||||||
|
STR_COLOR_MODE: "Režim barev"
|
||||||
|
STR_SCREEN_MARGIN: "Okraj obrazovky čtečky"
|
||||||
|
STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky"
|
||||||
|
STR_HYPHENATION: "Dělení slov"
|
||||||
|
STR_TIME_TO_SLEEP: "Čas do uspání"
|
||||||
|
STR_REFRESH_FREQ: "Frekvence obnovení"
|
||||||
|
STR_CALIBRE_SETTINGS: "Nastavení Calibre"
|
||||||
|
STR_KOREADER_SYNC: "KOReaderu Sync"
|
||||||
|
STR_CHECK_UPDATES: "Zkontrolovat aktualizace"
|
||||||
|
STR_LANGUAGE: "Jazyk"
|
||||||
|
STR_SELECT_WALLPAPER: "Vybrat tapetu"
|
||||||
|
STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Uživatelské jméno"
|
||||||
|
STR_PASSWORD: "Heslo"
|
||||||
|
STR_SYNC_SERVER_URL: "URL synch. serveru"
|
||||||
|
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
||||||
|
STR_AUTHENTICATE: "Ověření"
|
||||||
|
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
||||||
|
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
||||||
|
STR_FILENAME: "Název souboru"
|
||||||
|
STR_BINARY: "Binární"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Nastavte přihlašovací údaje"
|
||||||
|
STR_WIFI_CONN_FAILED: "Připojení k Wi-Fi selhalo"
|
||||||
|
STR_AUTHENTICATING: "Ověřování..."
|
||||||
|
STR_AUTH_SUCCESS: "Úspěšné ověření!"
|
||||||
|
STR_KOREADER_AUTH: "Ověření KOReaderu"
|
||||||
|
STR_SYNC_READY: "Synchronizace KOReaderu je připravena k použití"
|
||||||
|
STR_AUTH_FAILED: "Ověření selhalo"
|
||||||
|
STR_DONE: "Hotovo"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Tímto vymažete všechna data knih v mezipaměti."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Veškerý průběh čtení bude ztracen!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Knihy bude nutné znovu indexovat"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "při opětovném otevření."
|
||||||
|
STR_CLEARING_CACHE: "Mazání mezipaměti..."
|
||||||
|
STR_CACHE_CLEARED: "Mezipaměť vymazána"
|
||||||
|
STR_ITEMS_REMOVED: "položky odstraněny"
|
||||||
|
STR_FAILED_LOWER: "selhalo"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Vymazání mezipaměti se nezdařilo"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti naleznete v sériovém výstupu"
|
||||||
|
STR_DARK: "Tmavý"
|
||||||
|
STR_LIGHT: "Světlý"
|
||||||
|
STR_CUSTOM: "Vlastní"
|
||||||
|
STR_COVER: "Obálka"
|
||||||
|
STR_NONE_OPT: "Žádný"
|
||||||
|
STR_FIT: "Přizpůsobit"
|
||||||
|
STR_CROP: "Oříznout"
|
||||||
|
STR_NO_PROGRESS: "Žádný postup"
|
||||||
|
STR_FULL_OPT: "Plná"
|
||||||
|
STR_NEVER: "Nikdy"
|
||||||
|
STR_IN_READER: "Ve čtečce"
|
||||||
|
STR_ALWAYS: "Vždy"
|
||||||
|
STR_IGNORE: "Ignorovat"
|
||||||
|
STR_SLEEP: "Spánek"
|
||||||
|
STR_PAGE_TURN: "Otáčení stránek"
|
||||||
|
STR_PORTRAIT: "Na výšku"
|
||||||
|
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
||||||
|
STR_INVERTED: "Invertovaný"
|
||||||
|
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Zpět, Potvrdit, Vlevo, Vpravo"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Vlevo, Vpravo, Zpět, Potvrdit"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Vlevo, Zpět, Potvrdit, Vpravo"
|
||||||
|
STR_PREV_NEXT: "Předchozí/Další"
|
||||||
|
STR_NEXT_PREV: "Další/Předchozí"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Malý"
|
||||||
|
STR_MEDIUM: "Střední"
|
||||||
|
STR_LARGE: "Velký"
|
||||||
|
STR_X_LARGE: "Obří"
|
||||||
|
STR_TIGHT: "Těsný"
|
||||||
|
STR_NORMAL: "Normální"
|
||||||
|
STR_WIDE: "Široký"
|
||||||
|
STR_JUSTIFY: "Zarovnat do bloku"
|
||||||
|
STR_ALIGN_LEFT: "Vlevo"
|
||||||
|
STR_CENTER: "Na střed"
|
||||||
|
STR_ALIGN_RIGHT: "Vpravo"
|
||||||
|
STR_MIN_1: "1 min"
|
||||||
|
STR_MIN_5: "5 min"
|
||||||
|
STR_MIN_10: "10 min"
|
||||||
|
STR_MIN_15: "15 min"
|
||||||
|
STR_MIN_30: "30 min"
|
||||||
|
STR_PAGES_1: "1 stránka"
|
||||||
|
STR_PAGES_5: "5 stránek"
|
||||||
|
STR_PAGES_10: "10 stránek"
|
||||||
|
STR_PAGES_15: "15 stránek"
|
||||||
|
STR_PAGES_30: "30 stránek"
|
||||||
|
STR_UPDATE: "Aktualizace"
|
||||||
|
STR_CHECKING_UPDATE: "Kontrola aktualizací…"
|
||||||
|
STR_NEW_UPDATE: "Nová aktualizace k dispozici!"
|
||||||
|
STR_CURRENT_VERSION: "Aktuální verze:"
|
||||||
|
STR_NEW_VERSION: "Nová verze:"
|
||||||
|
STR_UPDATING: "Aktualizace..."
|
||||||
|
STR_NO_UPDATE: "Žádná aktualizace k dispozici"
|
||||||
|
STR_UPDATE_FAILED: "Aktualizace selhala"
|
||||||
|
STR_UPDATE_COMPLETE: "Aktualizace dokončena"
|
||||||
|
STR_POWER_ON_HINT: "Stiskněte a podržte tlačítko napájení pro opětovné zapnutí"
|
||||||
|
STR_EXTERNAL_FONT: "Externí písmo"
|
||||||
|
STR_BUILTIN_DISABLED: "Vestavěné (Zakázáno)"
|
||||||
|
STR_NO_ENTRIES: "Žádné položky nenalezeny"
|
||||||
|
STR_DOWNLOADING: "Stahování..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Stahování selhalo"
|
||||||
|
STR_ERROR_MSG: "Chyba:"
|
||||||
|
STR_UNNAMED: "Nepojmenované"
|
||||||
|
STR_NO_SERVER_URL: "Není nakonfigurována adresa URL serveru"
|
||||||
|
STR_FETCH_FEED_FAILED: "Načtení kanálu se nezdařilo"
|
||||||
|
STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo"
|
||||||
|
STR_NETWORK_PREFIX: "Síť:"
|
||||||
|
STR_IP_ADDRESS_PREFIX: "IP adresa:"
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "nebo naskenujte QR kód telefonem pro připojení k Wi-Fi."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení"
|
||||||
|
STR_SD_CARD: "SD karta"
|
||||||
|
STR_BACK: "« Zpět"
|
||||||
|
STR_EXIT: "« Konec"
|
||||||
|
STR_HOME: "« Domů"
|
||||||
|
STR_SAVE: "« Uložit"
|
||||||
|
STR_SELECT: "Vybrat"
|
||||||
|
STR_TOGGLE: "Přepnout"
|
||||||
|
STR_CONFIRM: "Potvrdit"
|
||||||
|
STR_CANCEL: "Zrušit"
|
||||||
|
STR_CONNECT: "Připojit"
|
||||||
|
STR_OPEN: "Otevřít"
|
||||||
|
STR_DOWNLOAD: "Stáhnout"
|
||||||
|
STR_RETRY: "Zkusit znovu"
|
||||||
|
STR_YES: "Ano"
|
||||||
|
STR_NO: "Ne"
|
||||||
|
STR_STATE_ON: "ZAP"
|
||||||
|
STR_STATE_OFF: "VYP"
|
||||||
|
STR_SET: "Nastavit"
|
||||||
|
STR_NOT_SET: "Nenastaveno"
|
||||||
|
STR_DIR_LEFT: "Vlevo"
|
||||||
|
STR_DIR_RIGHT: "Vpravo"
|
||||||
|
STR_DIR_UP: "Nahoru"
|
||||||
|
STR_DIR_DOWN: "Dolů"
|
||||||
|
STR_CAPS_ON: "PÍSMO"
|
||||||
|
STR_CAPS_OFF: "písmo"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[ZAP]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku"
|
||||||
|
STR_FILTER_CONTRAST: "Kontrast"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Plný s procenty"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Plný s pruhem knih"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Pouze pruh knih"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol"
|
||||||
|
STR_UI_THEME: "Šablona rozhraní"
|
||||||
|
STR_THEME_CLASSIC: "Klasická"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
|
||||||
|
STR_OPDS_BROWSER: "Prohlížeč OPDS"
|
||||||
|
STR_COVER_CUSTOM: "Obálka + Vlastní"
|
||||||
|
STR_RECENTS: "Nedávné"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Nedávné knihy"
|
||||||
|
STR_NO_RECENT_BOOKS: "Žádné nedávné knihy"
|
||||||
|
STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Zapomenout síť a odstranit uložené heslo?"
|
||||||
|
STR_FORGET_BUTTON: "Zapomenout na síť"
|
||||||
|
STR_CALIBRE_STARTING: "Spuštění Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Nastavení"
|
||||||
|
STR_CALIBRE_STATUS: "Stav"
|
||||||
|
STR_CLEAR_BUTTON: "Vymazat"
|
||||||
|
STR_DEFAULT_VALUE: "Výchozí"
|
||||||
|
STR_REMAP_PROMPT: "Stiskněte přední tlačítko pro každou roli"
|
||||||
|
STR_UNASSIGNED: "Nepřiřazeno"
|
||||||
|
STR_ALREADY_ASSIGNED: "Již přiřazeno"
|
||||||
|
STR_REMAP_RESET_HINT: "Boční tlačítko Nahoru: Obnovit výchozí rozvržení"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Boční tlačítko Dolů: Zrušit přemapování"
|
||||||
|
STR_HW_BACK_LABEL: "Zpět (1. tlačítko)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Potvrdit (2. tlačítko)"
|
||||||
|
STR_HW_LEFT_LABEL: "Vlevo (3. tlačítko)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačítko)"
|
||||||
|
STR_GO_TO_PERCENT: "Přejít na %"
|
||||||
|
STR_GO_HOME_BUTTON: "Přejít Domů"
|
||||||
|
STR_SYNC_PROGRESS: "Průběh synchronizace"
|
||||||
|
STR_DELETE_CACHE: "Smazat mezipaměť knihy"
|
||||||
|
STR_CHAPTER_PREFIX: "Kapitola:"
|
||||||
|
STR_PAGES_SEPARATOR: "stránek |"
|
||||||
|
STR_BOOK_PREFIX: "Kniha:"
|
||||||
|
STR_KBD_SHIFT: "shift"
|
||||||
|
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||||
|
STR_KBD_LOCK: "ZÁMEK"
|
||||||
|
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
|
||||||
|
STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %"
|
||||||
|
STR_SYNCING_TIME: "Čas synchronizace..."
|
||||||
|
STR_CALC_HASH: "Výpočet hashe dokumentu..."
|
||||||
|
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
|
||||||
|
STR_FETCH_PROGRESS: "Načítání vzdáleného průběhu..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Průběh nahrávání..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Přihlašovací údaje nejsou nakonfigurovány"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Nastavit účet KOReader v Nastavení"
|
||||||
|
STR_PROGRESS_FOUND: "Nalezen průběh!"
|
||||||
|
STR_REMOTE_LABEL: "Vzdálené:"
|
||||||
|
STR_LOCAL_LABEL: "Lokální:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Stránka %d, celkově %.2f%%"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stránka %d/%d, celkově %.2f%%"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " Od: %s"
|
||||||
|
STR_APPLY_REMOTE: "Použít vzdálený postup"
|
||||||
|
STR_UPLOAD_LOCAL: "Nahrát lokální postup"
|
||||||
|
STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený postup"
|
||||||
|
STR_UPLOAD_PROMPT: "Nahrát aktuální pozici?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Postup nahrán!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Synchronizace se nezdařila"
|
||||||
|
STR_SECTION_PREFIX: "Sekce"
|
||||||
|
STR_UPLOAD: "Nahrát"
|
||||||
|
STR_BOOK_S_STYLE: "Styl knihy"
|
||||||
|
STR_EMBEDDED_STYLE: "Vložený styl"
|
||||||
|
STR_OPDS_SERVER_URL: "URL serveru OPDS"
|
||||||
317
lib/I18n/translations/english.yaml
Normal file
317
lib/I18n/translations/english.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "English"
|
||||||
|
_language_code: "ENGLISH"
|
||||||
|
_order: "0"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "BOOTING"
|
||||||
|
STR_SLEEPING: "SLEEPING"
|
||||||
|
STR_ENTERING_SLEEP: "Entering Sleep..."
|
||||||
|
STR_BROWSE_FILES: "Browse Files"
|
||||||
|
STR_FILE_TRANSFER: "File Transfer"
|
||||||
|
STR_SETTINGS_TITLE: "Settings"
|
||||||
|
STR_CALIBRE_LIBRARY: "Calibre Library"
|
||||||
|
STR_CONTINUE_READING: "Continue Reading"
|
||||||
|
STR_NO_OPEN_BOOK: "No open book"
|
||||||
|
STR_START_READING: "Start reading below"
|
||||||
|
STR_BOOKS: "Books"
|
||||||
|
STR_NO_BOOKS_FOUND: "No books found"
|
||||||
|
STR_SELECT_CHAPTER: "Select Chapter"
|
||||||
|
STR_NO_CHAPTERS: "No chapters"
|
||||||
|
STR_END_OF_BOOK: "End of book"
|
||||||
|
STR_EMPTY_CHAPTER: "Empty chapter"
|
||||||
|
STR_INDEXING: "Indexing..."
|
||||||
|
STR_MEMORY_ERROR: "Memory error"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Page load error"
|
||||||
|
STR_EMPTY_FILE: "Empty file"
|
||||||
|
STR_OUT_OF_BOUNDS: "Out of bounds"
|
||||||
|
STR_LOADING: "Loading..."
|
||||||
|
STR_LOAD_XTC_FAILED: "Failed to load XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Failed to load TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Failed to load EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "SD card error"
|
||||||
|
STR_WIFI_NETWORKS: "WiFi Networks"
|
||||||
|
STR_NO_NETWORKS: "No networks found"
|
||||||
|
STR_NETWORKS_FOUND: "%zu networks found"
|
||||||
|
STR_SCANNING: "Scanning..."
|
||||||
|
STR_CONNECTING: "Connecting..."
|
||||||
|
STR_CONNECTED: "Connected!"
|
||||||
|
STR_CONNECTION_FAILED: "Connection Failed"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Connection timeout"
|
||||||
|
STR_FORGET_NETWORK: "Forget Network?"
|
||||||
|
STR_SAVE_PASSWORD: "Save password for next time?"
|
||||||
|
STR_REMOVE_PASSWORD: "Remove saved password?"
|
||||||
|
STR_PRESS_OK_SCAN: "Press OK to scan again"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Press any button to continue"
|
||||||
|
STR_SELECT_HINT: "LEFT/RIGHT: Select | OK: Confirm"
|
||||||
|
STR_HOW_CONNECT: "How would you like to connect?"
|
||||||
|
STR_JOIN_NETWORK: "Join a Network"
|
||||||
|
STR_CREATE_HOTSPOT: "Create Hotspot"
|
||||||
|
STR_JOIN_DESC: "Connect to an existing WiFi network"
|
||||||
|
STR_HOTSPOT_DESC: "Create a WiFi network others can join"
|
||||||
|
STR_STARTING_HOTSPOT: "Starting Hotspot..."
|
||||||
|
STR_HOTSPOT_MODE: "Hotspot Mode"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network"
|
||||||
|
STR_OPEN_URL_HINT: "Open this URL in your browser"
|
||||||
|
STR_OR_HTTP_PREFIX: "or http://"
|
||||||
|
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
|
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||||
|
STR_CONNECT_WIRELESS: "Connect as Wireless Device"
|
||||||
|
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||||
|
STR_MAC_ADDRESS: "MAC address:"
|
||||||
|
STR_CHECKING_WIFI: "Checking WiFi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password"
|
||||||
|
STR_ENTER_TEXT: "Enter Text"
|
||||||
|
STR_TO_PREFIX: "to "
|
||||||
|
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Connecting to "
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Connected to "
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Waiting for commands..."
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Connection failed, retrying)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre disconnected"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Waiting for transfer..."
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "If transfer fails, enable\\n'Ignore free space' in Calibre's\\nSmartDevice plugin settings."
|
||||||
|
STR_CALIBRE_RECEIVING: "Receiving: "
|
||||||
|
STR_CALIBRE_RECEIVED: "Received: "
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Waiting for more..."
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Failed to create file"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Password required"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer interrupted"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\""
|
||||||
|
STR_CAT_DISPLAY: "Display"
|
||||||
|
STR_CAT_READER: "Reader"
|
||||||
|
STR_CAT_CONTROLS: "Controls"
|
||||||
|
STR_CAT_SYSTEM: "System"
|
||||||
|
STR_SLEEP_SCREEN: "Sleep Screen"
|
||||||
|
STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
|
||||||
|
STR_STATUS_BAR: "Status Bar"
|
||||||
|
STR_HIDE_BATTERY: "Hide Battery %"
|
||||||
|
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
|
||||||
|
STR_TEXT_AA: "Text Anti-Aliasing"
|
||||||
|
STR_SHORT_PWR_BTN: "Short Power Button Click"
|
||||||
|
STR_ORIENTATION: "Reading Orientation"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Front Button Layout"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
||||||
|
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
|
||||||
|
STR_FONT_FAMILY: "Reader Font Family"
|
||||||
|
STR_EXT_READER_FONT: "External Reader Font"
|
||||||
|
STR_EXT_CHINESE_FONT: "Reader Font"
|
||||||
|
STR_EXT_UI_FONT: "UI Font"
|
||||||
|
STR_FONT_SIZE: "UI Font Size"
|
||||||
|
STR_LINE_SPACING: "Reader Line Spacing"
|
||||||
|
STR_ASCII_LETTER_SPACING: "ASCII Letter Spacing"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "ASCII Digit Spacing"
|
||||||
|
STR_CJK_SPACING: "CJK Spacing"
|
||||||
|
STR_COLOR_MODE: "Color Mode"
|
||||||
|
STR_SCREEN_MARGIN: "Reader Screen Margin"
|
||||||
|
STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
|
||||||
|
STR_HYPHENATION: "Hyphenation"
|
||||||
|
STR_TIME_TO_SLEEP: "Time to Sleep"
|
||||||
|
STR_REFRESH_FREQ: "Refresh Frequency"
|
||||||
|
STR_CALIBRE_SETTINGS: "Calibre Settings"
|
||||||
|
STR_KOREADER_SYNC: "KOReader Sync"
|
||||||
|
STR_CHECK_UPDATES: "Check for updates"
|
||||||
|
STR_LANGUAGE: "Language"
|
||||||
|
STR_SELECT_WALLPAPER: "Select Wallpaper"
|
||||||
|
STR_CLEAR_READING_CACHE: "Clear Reading Cache"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Username"
|
||||||
|
STR_PASSWORD: "Password"
|
||||||
|
STR_SYNC_SERVER_URL: "Sync Server URL"
|
||||||
|
STR_DOCUMENT_MATCHING: "Document Matching"
|
||||||
|
STR_AUTHENTICATE: "Authenticate"
|
||||||
|
STR_KOREADER_USERNAME: "KOReader Username"
|
||||||
|
STR_KOREADER_PASSWORD: "KOReader Password"
|
||||||
|
STR_FILENAME: "Filename"
|
||||||
|
STR_BINARY: "Binary"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Set credentials first"
|
||||||
|
STR_WIFI_CONN_FAILED: "WiFi connection failed"
|
||||||
|
STR_AUTHENTICATING: "Authenticating..."
|
||||||
|
STR_AUTH_SUCCESS: "Successfully authenticated!"
|
||||||
|
STR_KOREADER_AUTH: "KOReader Auth"
|
||||||
|
STR_SYNC_READY: "KOReader sync is ready to use"
|
||||||
|
STR_AUTH_FAILED: "Authentication Failed"
|
||||||
|
STR_DONE: "Done"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "when opened again."
|
||||||
|
STR_CLEARING_CACHE: "Clearing cache..."
|
||||||
|
STR_CACHE_CLEARED: "Cache Cleared"
|
||||||
|
STR_ITEMS_REMOVED: "items removed"
|
||||||
|
STR_FAILED_LOWER: "failed"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Failed to clear cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Check serial output for details"
|
||||||
|
STR_DARK: "Dark"
|
||||||
|
STR_LIGHT: "Light"
|
||||||
|
STR_CUSTOM: "Custom"
|
||||||
|
STR_COVER: "Cover"
|
||||||
|
STR_NONE_OPT: "None"
|
||||||
|
STR_FIT: "Fit"
|
||||||
|
STR_CROP: "Crop"
|
||||||
|
STR_NO_PROGRESS: "No Progress"
|
||||||
|
STR_FULL_OPT: "Full"
|
||||||
|
STR_NEVER: "Never"
|
||||||
|
STR_IN_READER: "In Reader"
|
||||||
|
STR_ALWAYS: "Always"
|
||||||
|
STR_IGNORE: "Ignore"
|
||||||
|
STR_SLEEP: "Sleep"
|
||||||
|
STR_PAGE_TURN: "Page Turn"
|
||||||
|
STR_PORTRAIT: "Portrait"
|
||||||
|
STR_LANDSCAPE_CW: "Landscape CW"
|
||||||
|
STR_INVERTED: "Inverted"
|
||||||
|
STR_LANDSCAPE_CCW: "Landscape CCW"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Bck, Cnfrm, Lft, Rght"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Lft, Rght, Bck, Cnfrm"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Lft, Bck, Cnfrm, Rght"
|
||||||
|
STR_PREV_NEXT: "Prev/Next"
|
||||||
|
STR_NEXT_PREV: "Next/Prev"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Small"
|
||||||
|
STR_MEDIUM: "Medium"
|
||||||
|
STR_LARGE: "Large"
|
||||||
|
STR_X_LARGE: "X Large"
|
||||||
|
STR_TIGHT: "Tight"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Wide"
|
||||||
|
STR_JUSTIFY: "Justify"
|
||||||
|
STR_ALIGN_LEFT: "Left"
|
||||||
|
STR_CENTER: "Center"
|
||||||
|
STR_ALIGN_RIGHT: "Right"
|
||||||
|
STR_MIN_1: "1 min"
|
||||||
|
STR_MIN_5: "5 min"
|
||||||
|
STR_MIN_10: "10 min"
|
||||||
|
STR_MIN_15: "15 min"
|
||||||
|
STR_MIN_30: "30 min"
|
||||||
|
STR_PAGES_1: "1 page"
|
||||||
|
STR_PAGES_5: "5 pages"
|
||||||
|
STR_PAGES_10: "10 pages"
|
||||||
|
STR_PAGES_15: "15 pages"
|
||||||
|
STR_PAGES_30: "30 pages"
|
||||||
|
STR_UPDATE: "Update"
|
||||||
|
STR_CHECKING_UPDATE: "Checking for update..."
|
||||||
|
STR_NEW_UPDATE: "New update available!"
|
||||||
|
STR_CURRENT_VERSION: "Current Version: "
|
||||||
|
STR_NEW_VERSION: "New Version: "
|
||||||
|
STR_UPDATING: "Updating..."
|
||||||
|
STR_NO_UPDATE: "No update available"
|
||||||
|
STR_UPDATE_FAILED: "Update failed"
|
||||||
|
STR_UPDATE_COMPLETE: "Update complete"
|
||||||
|
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
|
||||||
|
STR_EXTERNAL_FONT: "External Font"
|
||||||
|
STR_BUILTIN_DISABLED: "Built-in (Disabled)"
|
||||||
|
STR_NO_ENTRIES: "No entries found"
|
||||||
|
STR_DOWNLOADING: "Downloading..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Download failed"
|
||||||
|
STR_ERROR_MSG: "Error:"
|
||||||
|
STR_UNNAMED: "Unnamed"
|
||||||
|
STR_NO_SERVER_URL: "No server URL configured"
|
||||||
|
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||||
|
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||||
|
STR_NETWORK_PREFIX: "Network: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "IP Address: "
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Error: Network not found"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
|
||||||
|
STR_SD_CARD: "SD card"
|
||||||
|
STR_BACK: "« Back"
|
||||||
|
STR_EXIT: "« Exit"
|
||||||
|
STR_HOME: "« Home"
|
||||||
|
STR_SAVE: "« Save"
|
||||||
|
STR_SELECT: "Select"
|
||||||
|
STR_TOGGLE: "Toggle"
|
||||||
|
STR_CONFIRM: "Confirm"
|
||||||
|
STR_CANCEL: "Cancel"
|
||||||
|
STR_CONNECT: "Connect"
|
||||||
|
STR_OPEN: "Open"
|
||||||
|
STR_DOWNLOAD: "Download"
|
||||||
|
STR_RETRY: "Retry"
|
||||||
|
STR_YES: "Yes"
|
||||||
|
STR_NO: "No"
|
||||||
|
STR_STATE_ON: "ON"
|
||||||
|
STR_STATE_OFF: "OFF"
|
||||||
|
STR_SET: "Set"
|
||||||
|
STR_NOT_SET: "Not Set"
|
||||||
|
STR_DIR_LEFT: "Left"
|
||||||
|
STR_DIR_RIGHT: "Right"
|
||||||
|
STR_DIR_UP: "Up"
|
||||||
|
STR_DIR_DOWN: "Down"
|
||||||
|
STR_CAPS_ON: "CAPS"
|
||||||
|
STR_CAPS_OFF: "caps"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[ON]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
|
||||||
|
STR_FILTER_CONTRAST: "Contrast"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Percentage"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Full w/ Book Bar"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Book Bar Only"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar"
|
||||||
|
STR_UI_THEME: "UI Theme"
|
||||||
|
STR_THEME_CLASSIC: "Classic"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||||
|
STR_OPDS_BROWSER: "OPDS Browser"
|
||||||
|
STR_COVER_CUSTOM: "Cover + Custom"
|
||||||
|
STR_RECENTS: "Recents"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Recent Books"
|
||||||
|
STR_NO_RECENT_BOOKS: "No recent books"
|
||||||
|
STR_CALIBRE_DESC: "Use Calibre wireless device transfers"
|
||||||
|
STR_FORGET_AND_REMOVE: "Forget network and remove saved password?"
|
||||||
|
STR_FORGET_BUTTON: "Forget network"
|
||||||
|
STR_CALIBRE_STARTING: "Starting Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Setup"
|
||||||
|
STR_CALIBRE_STATUS: "Status"
|
||||||
|
STR_CLEAR_BUTTON: "Clear"
|
||||||
|
STR_DEFAULT_VALUE: "Default"
|
||||||
|
STR_REMAP_PROMPT: "Press a front button for each role"
|
||||||
|
STR_UNASSIGNED: "Unassigned"
|
||||||
|
STR_ALREADY_ASSIGNED: "Already assigned"
|
||||||
|
STR_REMAP_RESET_HINT: "Side button Up: Reset to default layout"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Side button Down: Cancel remapping"
|
||||||
|
STR_HW_BACK_LABEL: "Back (1st button)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
||||||
|
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
||||||
|
STR_GO_TO_PERCENT: "Go to %"
|
||||||
|
STR_GO_HOME_BUTTON: "Go Home"
|
||||||
|
STR_SYNC_PROGRESS: "Sync Progress"
|
||||||
|
STR_DELETE_CACHE: "Delete Book Cache"
|
||||||
|
STR_CHAPTER_PREFIX: "Chapter: "
|
||||||
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
|
STR_BOOK_PREFIX: "Book: "
|
||||||
|
STR_KBD_SHIFT: "shift"
|
||||||
|
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||||
|
STR_KBD_LOCK: "LOCK"
|
||||||
|
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
|
||||||
|
STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%"
|
||||||
|
STR_SYNCING_TIME: "Syncing time..."
|
||||||
|
STR_CALC_HASH: "Calculating document hash..."
|
||||||
|
STR_HASH_FAILED: "Failed to calculate document hash"
|
||||||
|
STR_FETCH_PROGRESS: "Fetching remote progress..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Uploading progress..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "No credentials configured"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Set up KOReader account in Settings"
|
||||||
|
STR_PROGRESS_FOUND: "Progress found!"
|
||||||
|
STR_REMOTE_LABEL: "Remote:"
|
||||||
|
STR_LOCAL_LABEL: "Local:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% overall"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% overall"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " From: %s"
|
||||||
|
STR_APPLY_REMOTE: "Apply remote progress"
|
||||||
|
STR_UPLOAD_LOCAL: "Upload local progress"
|
||||||
|
STR_NO_REMOTE_MSG: "No remote progress found"
|
||||||
|
STR_UPLOAD_PROMPT: "Upload current position?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Progress uploaded!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Sync failed"
|
||||||
|
STR_SECTION_PREFIX: "Section "
|
||||||
|
STR_UPLOAD: "Upload"
|
||||||
|
STR_BOOK_S_STYLE: "Book's Style"
|
||||||
|
STR_EMBEDDED_STYLE: "Embedded Style"
|
||||||
|
STR_OPDS_SERVER_URL: "OPDS Server URL"
|
||||||
317
lib/I18n/translations/french.yaml
Normal file
317
lib/I18n/translations/french.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Français"
|
||||||
|
_language_code: "FRENCH"
|
||||||
|
_order: "2"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "DÉMARRAGE EN COURS"
|
||||||
|
STR_SLEEPING: "VEILLE"
|
||||||
|
STR_ENTERING_SLEEP: "Mise en veille…"
|
||||||
|
STR_BROWSE_FILES: "Fichiers"
|
||||||
|
STR_FILE_TRANSFER: "Transfert"
|
||||||
|
STR_SETTINGS_TITLE: "Réglages"
|
||||||
|
STR_CALIBRE_LIBRARY: "Bibliothèque Calibre"
|
||||||
|
STR_CONTINUE_READING: "Continuer la lecture"
|
||||||
|
STR_NO_OPEN_BOOK: "Aucun livre ouvert"
|
||||||
|
STR_START_READING: "Lisez votre premier livre ci-dessous"
|
||||||
|
STR_BOOKS: "Livres"
|
||||||
|
STR_NO_BOOKS_FOUND: "Dossier vide"
|
||||||
|
STR_SELECT_CHAPTER: "Choix du chapitre"
|
||||||
|
STR_NO_CHAPTERS: "Aucun chapitre"
|
||||||
|
STR_END_OF_BOOK: "Fin du livre"
|
||||||
|
STR_EMPTY_CHAPTER: "Chapitre vide"
|
||||||
|
STR_INDEXING: "Indexation en cours…"
|
||||||
|
STR_MEMORY_ERROR: "Erreur de mémoire"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Erreur de chargement"
|
||||||
|
STR_EMPTY_FILE: "Fichier vide"
|
||||||
|
STR_OUT_OF_BOUNDS: "Dépassement de mémoire"
|
||||||
|
STR_LOADING: "Chargement…"
|
||||||
|
STR_LOAD_XTC_FAILED: "Erreur de chargement du fichier XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Erreur de chargement du fichier TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Erreur de chargement du fichier EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "Carte mémoire absente"
|
||||||
|
STR_WIFI_NETWORKS: "Réseaux WiFi"
|
||||||
|
STR_NO_NETWORKS: "Aucun réseau"
|
||||||
|
STR_NETWORKS_FOUND: "%zu réseaux"
|
||||||
|
STR_SCANNING: "Recherche de réseaux en cours…"
|
||||||
|
STR_CONNECTING: "Connexion en cours…"
|
||||||
|
STR_CONNECTED: "Connecté !"
|
||||||
|
STR_CONNECTION_FAILED: "Échec de la connexion"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Délai de connexion dépassé"
|
||||||
|
STR_FORGET_NETWORK: "Oublier ce réseau ?"
|
||||||
|
STR_SAVE_PASSWORD: "Enregistrer le mot de passe ?"
|
||||||
|
STR_REMOVE_PASSWORD: "Supprimer le mot de passe enregistré ?"
|
||||||
|
STR_PRESS_OK_SCAN: "Appuyez sur OK pour détecter à nouveau"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Appuyez sur une touche pour continuer"
|
||||||
|
STR_SELECT_HINT: "GAUCHE/DROITE: Sélectionner | OK: Valider"
|
||||||
|
STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?"
|
||||||
|
STR_JOIN_NETWORK: "Connexion à un réseau"
|
||||||
|
STR_CREATE_HOTSPOT: "Créer un point d’accès"
|
||||||
|
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
|
||||||
|
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis d’autres appareils"
|
||||||
|
STR_STARTING_HOTSPOT: "Création du point d’accès en cours…"
|
||||||
|
STR_HOTSPOT_MODE: "Mode point d’accès"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi"
|
||||||
|
STR_OPEN_URL_HINT: "Ouvrez cette URL dans votre navigateur"
|
||||||
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
|
STR_SCAN_QR_HINT: "ou scannez le QR code avec votre téléphone"
|
||||||
|
STR_CALIBRE_WIRELESS: "Connexion à Calibre sans fil"
|
||||||
|
STR_CALIBRE_WEB_URL: "URL Web Calibre"
|
||||||
|
STR_CONNECT_WIRELESS: "Se connecter comme appareil sans fil"
|
||||||
|
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
|
||||||
|
STR_MAC_ADDRESS: "Adresse MAC :"
|
||||||
|
STR_CHECKING_WIFI: "Vérification du réseau WiFi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Entrez le mot de passe WiFi"
|
||||||
|
STR_ENTER_TEXT: "Entrez le texte"
|
||||||
|
STR_TO_PREFIX: "à "
|
||||||
|
STR_CALIBRE_DISCOVERING: "Recherche de Calibre en cours…"
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Connexion à "
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Connecté à "
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "En attente de commandes…"
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Échec de la connexion, nouvelle tentative)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre déconnecté"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "En attente de transfert…"
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Si le transfert échoue, activez\\n’Ignorer l’espace libre’ dans les\\nparamètres du plugin SmartDevice de Calibre."
|
||||||
|
STR_CALIBRE_RECEIVING: "Réception : "
|
||||||
|
STR_CALIBRE_RECEIVED: "Reçus : "
|
||||||
|
STR_CALIBRE_WAITING_MORE: "En attente de données supplémentaires…"
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Échec de la création du fichier"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Mot de passe requis"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : ‘Envoyer vers l’appareil’"
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "“Gardez cet écran ouvert pendant le transfert”"
|
||||||
|
STR_CAT_DISPLAY: "Affichage"
|
||||||
|
STR_CAT_READER: "Lecteur"
|
||||||
|
STR_CAT_CONTROLS: "Commandes"
|
||||||
|
STR_CAT_SYSTEM: "Système"
|
||||||
|
STR_SLEEP_SCREEN: "Écran de veille"
|
||||||
|
STR_SLEEP_COVER_MODE: "Mode d’image de l’écran de veille"
|
||||||
|
STR_STATUS_BAR: "Barre d’état"
|
||||||
|
STR_HIDE_BATTERY: "Masquer % batterie"
|
||||||
|
STR_EXTRA_SPACING: "Espacement des paragraphes"
|
||||||
|
STR_TEXT_AA: "Lissage du texte"
|
||||||
|
STR_SHORT_PWR_BTN: "Appui court bout. alim."
|
||||||
|
STR_ORIENTATION: "Orientation de lecture"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Disposition des boutons avant"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Disposition des boutons latéraux"
|
||||||
|
STR_LONG_PRESS_SKIP: "Appui long pour saut de chapitre"
|
||||||
|
STR_FONT_FAMILY: "Police de caractères du lecteur"
|
||||||
|
STR_EXT_READER_FONT: "Police externe"
|
||||||
|
STR_EXT_CHINESE_FONT: "Police du lecteur"
|
||||||
|
STR_EXT_UI_FONT: "Police de l’interface"
|
||||||
|
STR_FONT_SIZE: "Taille du texte de l’interface"
|
||||||
|
STR_LINE_SPACING: "Espacement des lignes"
|
||||||
|
STR_ASCII_LETTER_SPACING: "Espacement des lettres ASCII"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "Espacement des chiffres ASCII"
|
||||||
|
STR_CJK_SPACING: "Espacement CJK"
|
||||||
|
STR_COLOR_MODE: "Mode couleur"
|
||||||
|
STR_SCREEN_MARGIN: "Marges du lecteur"
|
||||||
|
STR_PARA_ALIGNMENT: "Alignement des paragraphes"
|
||||||
|
STR_HYPHENATION: "Césure"
|
||||||
|
STR_TIME_TO_SLEEP: "Mise en veille automatique"
|
||||||
|
STR_REFRESH_FREQ: "Fréquence de rafraîchissement"
|
||||||
|
STR_CALIBRE_SETTINGS: "Réglages Calibre"
|
||||||
|
STR_KOREADER_SYNC: "Synchronisation KOReader"
|
||||||
|
STR_CHECK_UPDATES: "Mise à jour"
|
||||||
|
STR_LANGUAGE: "Langue"
|
||||||
|
STR_SELECT_WALLPAPER: "Fond d’écran"
|
||||||
|
STR_CLEAR_READING_CACHE: "Vider le cache de lecture"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Nom d’utilisateur"
|
||||||
|
STR_PASSWORD: "Mot de passe"
|
||||||
|
STR_SYNC_SERVER_URL: "URL du serveur"
|
||||||
|
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||||
|
STR_AUTHENTICATE: "Se connecter"
|
||||||
|
STR_KOREADER_USERNAME: "Nom d’utilisateur"
|
||||||
|
STR_KOREADER_PASSWORD: "Mot de passe"
|
||||||
|
STR_FILENAME: "Nom de fichier"
|
||||||
|
STR_BINARY: "Binaire"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Identifiants manquants"
|
||||||
|
STR_WIFI_CONN_FAILED: "Échec de connexion WiFi"
|
||||||
|
STR_AUTHENTICATING: "Connexion en cours…"
|
||||||
|
STR_AUTH_SUCCESS: "Connexion réussie !"
|
||||||
|
STR_KOREADER_AUTH: "Auth KOReader"
|
||||||
|
STR_SYNC_READY: "Synchronisation KOReader prête"
|
||||||
|
STR_AUTH_FAILED: "Échec de la connexion"
|
||||||
|
STR_DONE: "OK"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Le cache de votre bibliothèque sera entièrement vidé"
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Votre progression de lecture sera perdue !"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Les livres devront être réindexés"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "à leur prochaine ouverture."
|
||||||
|
STR_CLEARING_CACHE: "Suppression du cache…"
|
||||||
|
STR_CACHE_CLEARED: "Cache supprimé"
|
||||||
|
STR_ITEMS_REMOVED: "éléments supprimés"
|
||||||
|
STR_FAILED_LOWER: "ont échoué"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Échec de la suppression du cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Vérifiez la console série pour plus de détails"
|
||||||
|
STR_DARK: "Sombre"
|
||||||
|
STR_LIGHT: "Clair"
|
||||||
|
STR_CUSTOM: "Custom"
|
||||||
|
STR_COVER: "Couverture"
|
||||||
|
STR_NONE_OPT: "Aucun"
|
||||||
|
STR_FIT: "Ajusté"
|
||||||
|
STR_CROP: "Rogné"
|
||||||
|
STR_NO_PROGRESS: "Sans progression"
|
||||||
|
STR_FULL_OPT: "Complète"
|
||||||
|
STR_NEVER: "Jamais"
|
||||||
|
STR_IN_READER: "Dans le lecteur"
|
||||||
|
STR_ALWAYS: "Toujours"
|
||||||
|
STR_IGNORE: "Ignorer"
|
||||||
|
STR_SLEEP: "Mise en veille"
|
||||||
|
STR_PAGE_TURN: "Page suivante"
|
||||||
|
STR_PORTRAIT: "Portrait"
|
||||||
|
STR_LANDSCAPE_CW: "Paysage"
|
||||||
|
STR_INVERTED: "Inversé"
|
||||||
|
STR_LANDSCAPE_CCW: "Paysage inversé"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Ret, OK, Gauche, Droite"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Gauche, Droite, Ret, OK"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Gauche, Ret, OK, Droite"
|
||||||
|
STR_PREV_NEXT: "Prec/Suiv"
|
||||||
|
STR_NEXT_PREV: "Suiv/Prec"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Petite"
|
||||||
|
STR_MEDIUM: "Moyenne"
|
||||||
|
STR_LARGE: "Grande"
|
||||||
|
STR_X_LARGE: "T Grande"
|
||||||
|
STR_TIGHT: "Serré"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Large"
|
||||||
|
STR_JUSTIFY: "Justifier"
|
||||||
|
STR_ALIGN_LEFT: "Gauche"
|
||||||
|
STR_CENTER: "Centre"
|
||||||
|
STR_ALIGN_RIGHT: "Droite"
|
||||||
|
STR_MIN_1: "1 min"
|
||||||
|
STR_MIN_5: "5 min"
|
||||||
|
STR_MIN_10: "10 min"
|
||||||
|
STR_MIN_15: "15 min"
|
||||||
|
STR_MIN_30: "30 min"
|
||||||
|
STR_PAGES_1: "1 page"
|
||||||
|
STR_PAGES_5: "5 pages"
|
||||||
|
STR_PAGES_10: "10 pages"
|
||||||
|
STR_PAGES_15: "15 pages"
|
||||||
|
STR_PAGES_30: "30 pages"
|
||||||
|
STR_UPDATE: "Mise à jour"
|
||||||
|
STR_CHECKING_UPDATE: "Recherche de mises à jour en cours…"
|
||||||
|
STR_NEW_UPDATE: "Nouvelle mise à jour disponible !"
|
||||||
|
STR_CURRENT_VERSION: "Version actuelle :"
|
||||||
|
STR_NEW_VERSION: "Nouvelle version : "
|
||||||
|
STR_UPDATING: "Mise à jour en cours…"
|
||||||
|
STR_NO_UPDATE: "Aucune mise à jour disponible"
|
||||||
|
STR_UPDATE_FAILED: "Échec de la mise à jour"
|
||||||
|
STR_UPDATE_COMPLETE: "Mise à jour effectuée"
|
||||||
|
STR_POWER_ON_HINT: "Maintenir le bouton d’alimentation pour redémarrer"
|
||||||
|
STR_EXTERNAL_FONT: "Police externe"
|
||||||
|
STR_BUILTIN_DISABLED: "Intégrée (désactivée)"
|
||||||
|
STR_NO_ENTRIES: "Aucune entrée trouvée"
|
||||||
|
STR_DOWNLOADING: "Téléchargement en cours…"
|
||||||
|
STR_DOWNLOAD_FAILED: "Échec du téléchargement"
|
||||||
|
STR_ERROR_MSG: "Erreur : "
|
||||||
|
STR_UNNAMED: "Sans titre"
|
||||||
|
STR_NO_SERVER_URL: "Aucune URL serveur configurée"
|
||||||
|
STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux"
|
||||||
|
STR_PARSE_FEED_FAILED: "Échec de l’analyse du flux"
|
||||||
|
STR_NETWORK_PREFIX: "Réseau : "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Adresse IP : "
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Erreur : Délai de connexion dépassé"
|
||||||
|
STR_SD_CARD: "Carte SD"
|
||||||
|
STR_BACK: "« Retour"
|
||||||
|
STR_EXIT: "« Sortie"
|
||||||
|
STR_HOME: "« Accueil"
|
||||||
|
STR_SAVE: "« Sauver"
|
||||||
|
STR_SELECT: "OK"
|
||||||
|
STR_TOGGLE: "Modifier"
|
||||||
|
STR_CONFIRM: "Confirmer"
|
||||||
|
STR_CANCEL: "Annuler"
|
||||||
|
STR_CONNECT: "OK"
|
||||||
|
STR_OPEN: "Ouvrir"
|
||||||
|
STR_DOWNLOAD: "Télécharger"
|
||||||
|
STR_RETRY: "Réessayer"
|
||||||
|
STR_YES: "Oui"
|
||||||
|
STR_NO: "Non"
|
||||||
|
STR_STATE_ON: "ON"
|
||||||
|
STR_STATE_OFF: "OFF"
|
||||||
|
STR_SET: "Défini"
|
||||||
|
STR_NOT_SET: "Non défini"
|
||||||
|
STR_DIR_LEFT: "Gauche"
|
||||||
|
STR_DIR_RIGHT: "Droite"
|
||||||
|
STR_DIR_UP: "Haut"
|
||||||
|
STR_DIR_DOWN: "Bas"
|
||||||
|
STR_CAPS_ON: "MAJ"
|
||||||
|
STR_CAPS_OFF: "maj"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[ON]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Filtre affichage veille"
|
||||||
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Complète + %"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Complète + barre livre"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Barre livre"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre"
|
||||||
|
STR_UI_THEME: "Thème de l’interface"
|
||||||
|
STR_THEME_CLASSIC: "Classique"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant"
|
||||||
|
STR_OPDS_BROWSER: "Navigateur OPDS"
|
||||||
|
STR_COVER_CUSTOM: "Couverture + Custom"
|
||||||
|
STR_RECENTS: "Récents"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Livres récents"
|
||||||
|
STR_NO_RECENT_BOOKS: "Aucun livre récent"
|
||||||
|
STR_CALIBRE_DESC: "Utiliser les transferts sans fil Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Oublier le réseau et supprimer le mot de passe enregistré ?"
|
||||||
|
STR_FORGET_BUTTON: "Oublier le réseau"
|
||||||
|
STR_CALIBRE_STARTING: "Démarrage de Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Configuration"
|
||||||
|
STR_CALIBRE_STATUS: "Statut"
|
||||||
|
STR_CLEAR_BUTTON: "Effacer"
|
||||||
|
STR_DEFAULT_VALUE: "Défaut"
|
||||||
|
STR_REMAP_PROMPT: "Appuyez sur un bouton avant pour chaque rôle"
|
||||||
|
STR_UNASSIGNED: "Non assigné"
|
||||||
|
STR_ALREADY_ASSIGNED: "Déjà assigné"
|
||||||
|
STR_REMAP_RESET_HINT: "Bouton latéral haut : Réinitialiser"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Bouton latéral bas : Annuler le réglage"
|
||||||
|
STR_HW_BACK_LABEL: "Retour (1er bouton)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "OK (2ème bouton)"
|
||||||
|
STR_HW_LEFT_LABEL: "Gauche (3ème bouton)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Droite (4ème bouton)"
|
||||||
|
STR_GO_TO_PERCENT: "Aller à %"
|
||||||
|
STR_GO_HOME_BUTTON: "Aller à l’accueil"
|
||||||
|
STR_SYNC_PROGRESS: "Synchroniser la progression"
|
||||||
|
STR_DELETE_CACHE: "Supprimer le cache du livre"
|
||||||
|
STR_CHAPTER_PREFIX: "Chapitre : "
|
||||||
|
STR_PAGES_SEPARATOR: " pages | "
|
||||||
|
STR_BOOK_PREFIX: "Livre : "
|
||||||
|
STR_KBD_SHIFT: "maj"
|
||||||
|
STR_KBD_SHIFT_CAPS: "MAJ"
|
||||||
|
STR_KBD_LOCK: "VERR MAJ"
|
||||||
|
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
||||||
|
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
|
||||||
|
STR_SYNCING_TIME: "Synchronisation de l’heure…"
|
||||||
|
STR_CALC_HASH: "Calcul du hash du document…"
|
||||||
|
STR_HASH_FAILED: "Échec du calcul du hash du document"
|
||||||
|
STR_FETCH_PROGRESS: "Téléchargement de la progression…"
|
||||||
|
STR_UPLOAD_PROGRESS: "Envoi de la progression…"
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Aucun identifiant configuré"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Configurez le compte KOReader dans les réglages"
|
||||||
|
STR_PROGRESS_FOUND: "Progression trouvée !"
|
||||||
|
STR_REMOTE_LABEL: "En ligne :"
|
||||||
|
STR_LOCAL_LABEL: "Locale :"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% au total"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% au total"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " De : %s"
|
||||||
|
STR_APPLY_REMOTE: "Appliquer la progression en ligne"
|
||||||
|
STR_UPLOAD_LOCAL: "Envoyer la progression locale"
|
||||||
|
STR_NO_REMOTE_MSG: "Aucune progression en ligne trouvée"
|
||||||
|
STR_UPLOAD_PROMPT: "Envoyer la position actuelle ?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Progression envoyée !"
|
||||||
|
STR_SYNC_FAILED_MSG: "Échec de la synchronisation"
|
||||||
|
STR_SECTION_PREFIX: "Section "
|
||||||
|
STR_UPLOAD: "Envoi"
|
||||||
|
STR_BOOK_S_STYLE: "Style du livre"
|
||||||
|
STR_EMBEDDED_STYLE: "Style intégré"
|
||||||
|
STR_OPDS_SERVER_URL: "URL du serveur OPDS"
|
||||||
317
lib/I18n/translations/german.yaml
Normal file
317
lib/I18n/translations/german.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Deutsch"
|
||||||
|
_language_code: "GERMAN"
|
||||||
|
_order: "3"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "STARTEN"
|
||||||
|
STR_SLEEPING: "STANDBY"
|
||||||
|
STR_ENTERING_SLEEP: "Standby..."
|
||||||
|
STR_BROWSE_FILES: "Durchsuchen"
|
||||||
|
STR_FILE_TRANSFER: "Datentransfer"
|
||||||
|
STR_SETTINGS_TITLE: "Einstellungen"
|
||||||
|
STR_CALIBRE_LIBRARY: "Calibre-Bibliothek"
|
||||||
|
STR_CONTINUE_READING: "Weiterlesen"
|
||||||
|
STR_NO_OPEN_BOOK: "Aktuell kein Buch"
|
||||||
|
STR_START_READING: "Lesen beginnen"
|
||||||
|
STR_BOOKS: "Bücher"
|
||||||
|
STR_NO_BOOKS_FOUND: "Keine Bücher"
|
||||||
|
STR_SELECT_CHAPTER: "Kapitel auswählen"
|
||||||
|
STR_NO_CHAPTERS: "Keine Kapitel"
|
||||||
|
STR_END_OF_BOOK: "Buchende"
|
||||||
|
STR_EMPTY_CHAPTER: "Kapitelende"
|
||||||
|
STR_INDEXING: "Indexieren…"
|
||||||
|
STR_MEMORY_ERROR: "Speicherfehler"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Seitenladefehler"
|
||||||
|
STR_EMPTY_FILE: "Leere Datei"
|
||||||
|
STR_OUT_OF_BOUNDS: "Zu groß"
|
||||||
|
STR_LOADING: "Laden…"
|
||||||
|
STR_LOAD_XTC_FAILED: "Ladefehler bei XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Ladefehler bei TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Ladefehler bei EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "SD-Karten-Fehler"
|
||||||
|
STR_WIFI_NETWORKS: "WLAN-Netzwerke"
|
||||||
|
STR_NO_NETWORKS: "Kein WLAN gefunden"
|
||||||
|
STR_NETWORKS_FOUND: "%zu WLAN-Netzwerke gefunden"
|
||||||
|
STR_SCANNING: "Suchen..."
|
||||||
|
STR_CONNECTING: "Verbinden..."
|
||||||
|
STR_CONNECTED: "Verbunden!"
|
||||||
|
STR_CONNECTION_FAILED: "Verbindungsfehler"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Verbindungs-Timeout"
|
||||||
|
STR_FORGET_NETWORK: "WLAN vergessen?"
|
||||||
|
STR_SAVE_PASSWORD: "Passwort speichern?"
|
||||||
|
STR_REMOVE_PASSWORD: "Passwort entfernen?"
|
||||||
|
STR_PRESS_OK_SCAN: "OK für neue Suche"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Beliebige Taste drücken"
|
||||||
|
STR_SELECT_HINT: "links/rechts: Auswahl | OK: Best"
|
||||||
|
STR_HOW_CONNECT: "Wie möchtest du dich verbinden?"
|
||||||
|
STR_JOIN_NETWORK: "Netzwerk beitreten"
|
||||||
|
STR_CREATE_HOTSPOT: "Hotspot erstellen"
|
||||||
|
STR_JOIN_DESC: "Mit einem bestehenden WLAN verbinden"
|
||||||
|
STR_HOTSPOT_DESC: "WLAN für andere erstellen"
|
||||||
|
STR_STARTING_HOTSPOT: "Hotspot starten…"
|
||||||
|
STR_HOTSPOT_MODE: "Hotspot-Modus"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Gerät mit diesem WLAN verbinden"
|
||||||
|
STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
|
||||||
|
STR_OR_HTTP_PREFIX: "oder http://"
|
||||||
|
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
|
STR_CALIBRE_WEB_URL: "Calibre-Web-URL"
|
||||||
|
STR_CONNECT_WIRELESS: "Als Drahtlos-Gerät hinzufügen"
|
||||||
|
STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
|
||||||
|
STR_MAC_ADDRESS: "MAC-Adresse:"
|
||||||
|
STR_CHECKING_WIFI: "WLAN prüfen…"
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "WLAN-Passwort eingeben"
|
||||||
|
STR_ENTER_TEXT: "Text eingeben"
|
||||||
|
STR_TO_PREFIX: "bis"
|
||||||
|
STR_CALIBRE_DISCOVERING: "Calibre finden..."
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Verbinden mit"
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Verbunden mit"
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Auf Befehle warten…"
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Keine Verbindung, wiederholen)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre getrennt"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Auf Übertragung warten..."
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Bei Übertragungsfehler \\n'Freien Speicher ign.' in den\\nCalibre-Einstellungen einschalten."
|
||||||
|
STR_CALIBRE_RECEIVING: "Empfange:"
|
||||||
|
STR_CALIBRE_RECEIVED: "Empfangen:"
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Auf mehr warten…"
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Speicherfehler"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Passwort nötig"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Übertragung unterbrochen"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader-Plugin installieren"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Mit selbem WLAN verbinden"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"An Gerät senden\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "Bildschirm beim Senden offenlassen"
|
||||||
|
STR_CAT_DISPLAY: "Anzeige"
|
||||||
|
STR_CAT_READER: "Lesen"
|
||||||
|
STR_CAT_CONTROLS: "Bedienung"
|
||||||
|
STR_CAT_SYSTEM: "System"
|
||||||
|
STR_SLEEP_SCREEN: "Standby-Bild"
|
||||||
|
STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
|
||||||
|
STR_STATUS_BAR: "Statusleiste"
|
||||||
|
STR_HIDE_BATTERY: "Batterie % ausblenden"
|
||||||
|
STR_EXTRA_SPACING: "Absatzabstand"
|
||||||
|
STR_TEXT_AA: "Schriftglättung"
|
||||||
|
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
|
||||||
|
STR_ORIENTATION: "Leseausrichtung"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Vorderes Tastenlayout"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
||||||
|
STR_LONG_PRESS_SKIP: "Langes Drücken springt Kap."
|
||||||
|
STR_FONT_FAMILY: "Lese-Schriftfamilie"
|
||||||
|
STR_EXT_READER_FONT: "Externe Schriftart"
|
||||||
|
STR_EXT_CHINESE_FONT: "Lese-Schriftart"
|
||||||
|
STR_EXT_UI_FONT: "Menü-Schriftart"
|
||||||
|
STR_FONT_SIZE: "Schriftgröße"
|
||||||
|
STR_LINE_SPACING: "Lese-Zeilenabstand"
|
||||||
|
STR_ASCII_LETTER_SPACING: "ASCII-Zeichenabstand"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "ASCII-Ziffernabstand"
|
||||||
|
STR_CJK_SPACING: "CJK-Zeichenabstand"
|
||||||
|
STR_COLOR_MODE: "Farbmodus"
|
||||||
|
STR_SCREEN_MARGIN: "Lese-Seitenränder"
|
||||||
|
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
|
||||||
|
STR_HYPHENATION: "Silbentrennung"
|
||||||
|
STR_TIME_TO_SLEEP: "Standby nach"
|
||||||
|
STR_REFRESH_FREQ: "Anti-Ghosting nach"
|
||||||
|
STR_CALIBRE_SETTINGS: "Calibre-Einstellungen"
|
||||||
|
STR_KOREADER_SYNC: "KOReader-Synchr."
|
||||||
|
STR_CHECK_UPDATES: "Nach Updates suchen"
|
||||||
|
STR_LANGUAGE: "Sprache"
|
||||||
|
STR_SELECT_WALLPAPER: "Bildauswahl Standby"
|
||||||
|
STR_CLEAR_READING_CACHE: "Lese-Cache leeren"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Benutzername"
|
||||||
|
STR_PASSWORD: "Passwort nötig"
|
||||||
|
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
||||||
|
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
||||||
|
STR_AUTHENTICATE: "Authentifizieren"
|
||||||
|
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
||||||
|
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
||||||
|
STR_FILENAME: "Dateiname"
|
||||||
|
STR_BINARY: "Binärdatei"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Zuerst anmelden"
|
||||||
|
STR_WIFI_CONN_FAILED: "WLAN-Verbindung fehlgeschlagen"
|
||||||
|
STR_AUTHENTICATING: "Authentifizieren…"
|
||||||
|
STR_AUTH_SUCCESS: "Erfolgreich authentifiziert!"
|
||||||
|
STR_KOREADER_AUTH: "KOReader-Auth"
|
||||||
|
STR_SYNC_READY: "KOReader-Synchronisierung bereit"
|
||||||
|
STR_AUTH_FAILED: "Authentifizierung fehlg."
|
||||||
|
STR_DONE: "Erledigt"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Alle Buch-Caches werden geleert."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Lesefortschritt wird gelöscht!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Bücher müssen beim Öffnen"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "neu eingelesen werden."
|
||||||
|
STR_CLEARING_CACHE: "Cache leeren…"
|
||||||
|
STR_CACHE_CLEARED: "Cache geleert"
|
||||||
|
STR_ITEMS_REMOVED: "Einträge entfernt"
|
||||||
|
STR_FAILED_LOWER: "fehlgeschlagen"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Fehler beim Cache-Leeren"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Serielle Ausgabe prüfen"
|
||||||
|
STR_DARK: "Dunkel"
|
||||||
|
STR_LIGHT: "Hell"
|
||||||
|
STR_CUSTOM: "Eigenes"
|
||||||
|
STR_COVER: "Umschlag"
|
||||||
|
STR_NONE_OPT: "Leer"
|
||||||
|
STR_FIT: "Anpassen"
|
||||||
|
STR_CROP: "Zuschnitt"
|
||||||
|
STR_NO_PROGRESS: "Ohne Fortschr."
|
||||||
|
STR_FULL_OPT: "Vollst."
|
||||||
|
STR_NEVER: "Nie"
|
||||||
|
STR_IN_READER: "Beim Lesen"
|
||||||
|
STR_ALWAYS: "Immer"
|
||||||
|
STR_IGNORE: "Ignorieren"
|
||||||
|
STR_SLEEP: "Standby"
|
||||||
|
STR_PAGE_TURN: "Umblättern"
|
||||||
|
STR_PORTRAIT: "Hochformat"
|
||||||
|
STR_LANDSCAPE_CW: "Querformat rechts"
|
||||||
|
STR_INVERTED: "Invertiert"
|
||||||
|
STR_LANDSCAPE_CCW: "Querformat links"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Zurück, Bst, L, R"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "L, R, Zurück, Bst"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "L, Zurück, Bst, R"
|
||||||
|
STR_PREV_NEXT: "Zurück/Weiter"
|
||||||
|
STR_NEXT_PREV: "Weiter/Zuürck"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Klein"
|
||||||
|
STR_MEDIUM: "Mittel"
|
||||||
|
STR_LARGE: "Groß"
|
||||||
|
STR_X_LARGE: "Extragroß"
|
||||||
|
STR_TIGHT: "Eng"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Breit"
|
||||||
|
STR_JUSTIFY: "Blocksatz"
|
||||||
|
STR_ALIGN_LEFT: "Links"
|
||||||
|
STR_CENTER: "Zentriert"
|
||||||
|
STR_ALIGN_RIGHT: "Rechts"
|
||||||
|
STR_MIN_1: "1 Min"
|
||||||
|
STR_MIN_5: "5 Min"
|
||||||
|
STR_MIN_10: "10 Min"
|
||||||
|
STR_MIN_15: "15 Min"
|
||||||
|
STR_MIN_30: "30 Min"
|
||||||
|
STR_PAGES_1: "1 Seite"
|
||||||
|
STR_PAGES_5: "5 Seiten"
|
||||||
|
STR_PAGES_10: "10 Seiten"
|
||||||
|
STR_PAGES_15: "15 Seiten"
|
||||||
|
STR_PAGES_30: "30 Seiten"
|
||||||
|
STR_UPDATE: "Update"
|
||||||
|
STR_CHECKING_UPDATE: "Update suchen…"
|
||||||
|
STR_NEW_UPDATE: "Neues Update verfügbar!"
|
||||||
|
STR_CURRENT_VERSION: "Aktuelle Version:"
|
||||||
|
STR_NEW_VERSION: "Neue Version:"
|
||||||
|
STR_UPDATING: "Aktualisiere…"
|
||||||
|
STR_NO_UPDATE: "Kein Update verfügbar"
|
||||||
|
STR_UPDATE_FAILED: "Updatefehler"
|
||||||
|
STR_UPDATE_COMPLETE: "Update fertig"
|
||||||
|
STR_POWER_ON_HINT: "An-Knopf lang drücken, um neuzustarten"
|
||||||
|
STR_EXTERNAL_FONT: "Externe Schrift"
|
||||||
|
STR_BUILTIN_DISABLED: "Vorinstalliert (aus)"
|
||||||
|
STR_NO_ENTRIES: "Keine Einträge"
|
||||||
|
STR_DOWNLOADING: "Herunterladen…"
|
||||||
|
STR_DOWNLOAD_FAILED: "Ladefehler"
|
||||||
|
STR_ERROR_MSG: "Fehler:"
|
||||||
|
STR_UNNAMED: "Unbenannt"
|
||||||
|
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
|
||||||
|
STR_FETCH_FEED_FAILED: "Feedfehler"
|
||||||
|
STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
|
||||||
|
STR_NETWORK_PREFIX: "Netzwerk:"
|
||||||
|
STR_IP_ADDRESS_PREFIX: "IP-Adresse:"
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "oder QR-Code mit dem Handy scannen für WLAN."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Fehler: Allgemeiner Fehler"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Fehler: Kein Netzwerk"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Fehler: Zeitüberschreitung"
|
||||||
|
STR_SD_CARD: "SD-Karte"
|
||||||
|
STR_BACK: "« Zurück"
|
||||||
|
STR_EXIT: "« Verlassen"
|
||||||
|
STR_HOME: "« Start"
|
||||||
|
STR_SAVE: "« Speichern"
|
||||||
|
STR_SELECT: "Auswahl"
|
||||||
|
STR_TOGGLE: "Ändern"
|
||||||
|
STR_CONFIRM: "Bestätigen"
|
||||||
|
STR_CANCEL: "Abbrechen"
|
||||||
|
STR_CONNECT: "Verbinden"
|
||||||
|
STR_OPEN: "Öffnen"
|
||||||
|
STR_DOWNLOAD: "Herunterladen"
|
||||||
|
STR_RETRY: "Wiederh."
|
||||||
|
STR_YES: "Ja"
|
||||||
|
STR_NO: "Nein"
|
||||||
|
STR_STATE_ON: "An"
|
||||||
|
STR_STATE_OFF: "Aus"
|
||||||
|
STR_SET: "Gesetzt"
|
||||||
|
STR_NOT_SET: "Leer"
|
||||||
|
STR_DIR_LEFT: "Links"
|
||||||
|
STR_DIR_RIGHT: "Rechts"
|
||||||
|
STR_DIR_UP: "Hoch"
|
||||||
|
STR_DIR_DOWN: "Runter"
|
||||||
|
STR_CAPS_ON: "UMSCH"
|
||||||
|
STR_CAPS_OFF: "umsch"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[AN]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Standby-Coverfilter"
|
||||||
|
STR_FILTER_CONTRAST: "Kontrast"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Komplett + Prozent"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Komplett + Buch"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Nur Buch"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel"
|
||||||
|
STR_UI_THEME: "System-Design"
|
||||||
|
STR_THEME_CLASSIC: "Klassisch"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
|
||||||
|
STR_OPDS_BROWSER: "OPDS-Browser"
|
||||||
|
STR_COVER_CUSTOM: "Umschlag + Eigenes"
|
||||||
|
STR_RECENTS: "Zuletzt"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Zuletzt gelesen"
|
||||||
|
STR_NO_RECENT_BOOKS: "Keine Bücher"
|
||||||
|
STR_CALIBRE_DESC: "Calibre-Übertragung (WLAN)"
|
||||||
|
STR_FORGET_AND_REMOVE: "WLAN entfernen & Passwort löschen?"
|
||||||
|
STR_FORGET_BUTTON: "WLAN entfernen"
|
||||||
|
STR_CALIBRE_STARTING: "Calibre starten…"
|
||||||
|
STR_CALIBRE_SETUP: "Installation"
|
||||||
|
STR_CALIBRE_STATUS: "Status"
|
||||||
|
STR_CLEAR_BUTTON: "Leeren"
|
||||||
|
STR_DEFAULT_VALUE: "Standard"
|
||||||
|
STR_REMAP_PROMPT: "Entsprechende Vordertaste drücken"
|
||||||
|
STR_UNASSIGNED: "Leer"
|
||||||
|
STR_ALREADY_ASSIGNED: "Bereits zugeordnet"
|
||||||
|
STR_REMAP_RESET_HINT: "Seitentaste hoch: Standard"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Seitentaste runter: Abbrechen"
|
||||||
|
STR_HW_BACK_LABEL: "Zurück (1. Taste)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Bestätigen (2. Taste)"
|
||||||
|
STR_HW_LEFT_LABEL: "Links (3. Taste)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Rechts (4. Taste)"
|
||||||
|
STR_GO_TO_PERCENT: "Gehe zu %"
|
||||||
|
STR_GO_HOME_BUTTON: "Zum Anfang"
|
||||||
|
STR_SYNC_PROGRESS: "Fortschritt synchronisieren"
|
||||||
|
STR_DELETE_CACHE: "Buch-Cache leeren"
|
||||||
|
STR_CHAPTER_PREFIX: "Kapitel:"
|
||||||
|
STR_PAGES_SEPARATOR: " Seiten | "
|
||||||
|
STR_BOOK_PREFIX: "Buch: "
|
||||||
|
STR_KBD_SHIFT: "umsch"
|
||||||
|
STR_KBD_SHIFT_CAPS: "UMSCH"
|
||||||
|
STR_KBD_LOCK: "FESTST"
|
||||||
|
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
|
||||||
|
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
|
||||||
|
STR_SYNCING_TIME: "Zeit synchonisieren…"
|
||||||
|
STR_CALC_HASH: "Dokument-Hash berechnen…"
|
||||||
|
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
|
||||||
|
STR_FETCH_PROGRESS: "Externen Fortschritt abrufen..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Fortschritt hochladen…"
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Zugangsdaten fehlen"
|
||||||
|
STR_KOREADER_SETUP_HINT: "KOReader-Konto unter Einst. anlegen"
|
||||||
|
STR_PROGRESS_FOUND: "Gefunden!"
|
||||||
|
STR_REMOTE_LABEL: "Extern:"
|
||||||
|
STR_LOCAL_LABEL: "Lokal:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: " Seite %d, %.2f%% insgesamt"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: " Seite %d/%d, %.2f%% insgesamt"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " Von: %s"
|
||||||
|
STR_APPLY_REMOTE: "Ext. Fortschritt übern."
|
||||||
|
STR_UPLOAD_LOCAL: "Lokalen Fortschritt hochl."
|
||||||
|
STR_NO_REMOTE_MSG: "Kein externer Fortschritt"
|
||||||
|
STR_UPLOAD_PROMPT: "Aktuelle Position hochladen?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Hochgeladen!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Fehlgeschlagen"
|
||||||
|
STR_SECTION_PREFIX: "Abschnitt"
|
||||||
|
STR_UPLOAD: "Hochladen"
|
||||||
|
STR_BOOK_S_STYLE: "Buch-Stil"
|
||||||
|
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
|
||||||
|
STR_OPDS_SERVER_URL: "OPDS-Server-URL"
|
||||||
317
lib/I18n/translations/portuguese.yaml
Normal file
317
lib/I18n/translations/portuguese.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Português (Brasil)"
|
||||||
|
_language_code: "PORTUGUESE"
|
||||||
|
_order: "5"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "INICIANDO"
|
||||||
|
STR_SLEEPING: "EM REPOUSO"
|
||||||
|
STR_ENTERING_SLEEP: "Entrando em repouso..."
|
||||||
|
STR_BROWSE_FILES: "Arquivos"
|
||||||
|
STR_FILE_TRANSFER: "Transferência"
|
||||||
|
STR_SETTINGS_TITLE: "Configurações"
|
||||||
|
STR_CALIBRE_LIBRARY: "Biblioteca do Calibre"
|
||||||
|
STR_CONTINUE_READING: "Continuar lendo"
|
||||||
|
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
|
||||||
|
STR_START_READING: "Comece a ler abaixo"
|
||||||
|
STR_BOOKS: "Livros"
|
||||||
|
STR_NO_BOOKS_FOUND: "Nenhum livro encontrado"
|
||||||
|
STR_SELECT_CHAPTER: "Escolher capítulo"
|
||||||
|
STR_NO_CHAPTERS: "Sem capítulos"
|
||||||
|
STR_END_OF_BOOK: "Fim do livro"
|
||||||
|
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||||
|
STR_INDEXING: "Indexando..."
|
||||||
|
STR_MEMORY_ERROR: "Erro de memória"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Erro página"
|
||||||
|
STR_EMPTY_FILE: "Arquivo vazio"
|
||||||
|
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||||
|
STR_LOADING: "Carregando..."
|
||||||
|
STR_LOAD_XTC_FAILED: "Falha ao carregar XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Falha ao carregar TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Falha ao carregar EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "Erro no cartão SD"
|
||||||
|
STR_WIFI_NETWORKS: "Redes Wi‑Fi"
|
||||||
|
STR_NO_NETWORKS: "Sem redes"
|
||||||
|
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||||
|
STR_SCANNING: "Procurando..."
|
||||||
|
STR_CONNECTING: "Conectando..."
|
||||||
|
STR_CONNECTED: "Conectado!"
|
||||||
|
STR_CONNECTION_FAILED: "Falha na conexão"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Tempo limite conexão"
|
||||||
|
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||||
|
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
|
||||||
|
STR_REMOVE_PASSWORD: "Remover senha salva?"
|
||||||
|
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Pressione qualquer botão continuar"
|
||||||
|
STR_SELECT_HINT: "ESQ/DIR: Escolher | OK: Confirmar"
|
||||||
|
STR_HOW_CONNECT: "Como você gostaria se conectar?"
|
||||||
|
STR_JOIN_NETWORK: "Entrar em uma rede"
|
||||||
|
STR_CREATE_HOTSPOT: "Criar hotspot"
|
||||||
|
STR_JOIN_DESC: "Conecte-se a uma rede Wi‑Fi existente"
|
||||||
|
STR_HOTSPOT_DESC: "Crie uma rede Wi‑Fi outras pessoas entrarem"
|
||||||
|
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
||||||
|
STR_HOTSPOT_MODE: "Modo hotspot"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi‑Fi"
|
||||||
|
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
|
||||||
|
STR_OR_HTTP_PREFIX: "ou http://"
|
||||||
|
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre sem fio"
|
||||||
|
STR_CALIBRE_WEB_URL: "URL do Calibre Web"
|
||||||
|
STR_CONNECT_WIRELESS: "Conectar como dispositivo sem fio"
|
||||||
|
STR_NETWORK_LEGEND: "* = Criptografada | + = Salva"
|
||||||
|
STR_MAC_ADDRESS: "Endereço MAC:"
|
||||||
|
STR_CHECKING_WIFI: "Verificando Wi‑Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi‑Fi"
|
||||||
|
STR_ENTER_TEXT: "Inserir texto"
|
||||||
|
STR_TO_PREFIX: "para"
|
||||||
|
STR_CALIBRE_DISCOVERING: "Procurando o Calibre..."
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Conectando a"
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Conectado a"
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Aguardando comandos..."
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Falha conexão, tentando novamente)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Aguardando transferência..."
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Se a transferência falhar, ative\n\\n'Ignorar espaço livre'\\n nas \\nconfigurações do\nplugin SmartDevice\\n Calibre."
|
||||||
|
STR_CALIBRE_RECEIVING: "Recebendo:"
|
||||||
|
STR_CALIBRE_RECEIVED: "Recebido:"
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Aguardando mais..."
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Falha ao criar o arquivo"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Senha obrigatória"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transf. interrompida"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Esteja mesma rede Wi‑Fi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
|
||||||
|
STR_CAT_DISPLAY: "Tela"
|
||||||
|
STR_CAT_READER: "Leitor"
|
||||||
|
STR_CAT_CONTROLS: "Controles"
|
||||||
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
|
STR_SLEEP_SCREEN: "Tela de repouso"
|
||||||
|
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
|
||||||
|
STR_STATUS_BAR: "Barra de status"
|
||||||
|
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||||
|
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
|
||||||
|
STR_TEXT_AA: "Suavização de texto"
|
||||||
|
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
||||||
|
STR_ORIENTATION: "Orientação de leitura"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Disposição botões frontais"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
||||||
|
STR_LONG_PRESS_SKIP: "Pular capítulo com pressão longa"
|
||||||
|
STR_FONT_FAMILY: "Fonte do leitor"
|
||||||
|
STR_EXT_READER_FONT: "Fonte leitor externo"
|
||||||
|
STR_EXT_CHINESE_FONT: "Fonte do leitor"
|
||||||
|
STR_EXT_UI_FONT: "Fonte da interface"
|
||||||
|
STR_FONT_SIZE: "Tam. fonte UI"
|
||||||
|
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||||
|
STR_ASCII_LETTER_SPACING: "Espaçamento letras ASCII"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "Espaçamento dígitos ASCII"
|
||||||
|
STR_CJK_SPACING: "Espaçamento CJK"
|
||||||
|
STR_COLOR_MODE: "Modo de cor"
|
||||||
|
STR_SCREEN_MARGIN: "Margens da tela"
|
||||||
|
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
|
||||||
|
STR_HYPHENATION: "Hifenização"
|
||||||
|
STR_TIME_TO_SLEEP: "Tempo para repousar"
|
||||||
|
STR_REFRESH_FREQ: "Frequência atualização"
|
||||||
|
STR_CALIBRE_SETTINGS: "Configuração do Calibre"
|
||||||
|
STR_KOREADER_SYNC: "Sincronização KOReader"
|
||||||
|
STR_CHECK_UPDATES: "Verificar atualizações"
|
||||||
|
STR_LANGUAGE: "Idioma"
|
||||||
|
STR_SELECT_WALLPAPER: "Escolher papel parede"
|
||||||
|
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Nome de usuário"
|
||||||
|
STR_PASSWORD: "Senha"
|
||||||
|
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
||||||
|
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
||||||
|
STR_AUTHENTICATE: "Autenticar"
|
||||||
|
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
||||||
|
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
||||||
|
STR_FILENAME: "Nome do arquivo"
|
||||||
|
STR_BINARY: "Binário"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
|
||||||
|
STR_WIFI_CONN_FAILED: "Falha na conexão Wi‑Fi"
|
||||||
|
STR_AUTHENTICATING: "Autenticando..."
|
||||||
|
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
|
||||||
|
STR_KOREADER_AUTH: "Autenticação KOReader"
|
||||||
|
STR_SYNC_READY: "A sincronização KOReader está pronta uso"
|
||||||
|
STR_AUTH_FAILED: "Falha na autenticação"
|
||||||
|
STR_DONE: "Feito"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Isso vai limpar todos os dados livros em cache."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Os livros precisarão ser reindexados"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
|
||||||
|
STR_CLEARING_CACHE: "Limpando cache..."
|
||||||
|
STR_CACHE_CLEARED: "Cache limpo"
|
||||||
|
STR_ITEMS_REMOVED: "itens removidos"
|
||||||
|
STR_FAILED_LOWER: "falhou"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Falha ao limpar o cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Ver saída serial"
|
||||||
|
STR_DARK: "Escuro"
|
||||||
|
STR_LIGHT: "Claro"
|
||||||
|
STR_CUSTOM: "Personalizado"
|
||||||
|
STR_COVER: "Capa"
|
||||||
|
STR_NONE_OPT: "Nenhum"
|
||||||
|
STR_FIT: "Ajustar"
|
||||||
|
STR_CROP: "Recortar"
|
||||||
|
STR_NO_PROGRESS: "Sem progresso"
|
||||||
|
STR_FULL_OPT: "Completo"
|
||||||
|
STR_NEVER: "Nunca"
|
||||||
|
STR_IN_READER: "No leitor"
|
||||||
|
STR_ALWAYS: "Sempre"
|
||||||
|
STR_IGNORE: "Ignorar"
|
||||||
|
STR_SLEEP: "Repouso"
|
||||||
|
STR_PAGE_TURN: "Virar página"
|
||||||
|
STR_PORTRAIT: "Retrato"
|
||||||
|
STR_LANDSCAPE_CW: "Paisagem H"
|
||||||
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_LANDSCAPE_CCW: "Paisagem AH"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Vol, Conf, Esq, Dir"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Esq, Dir, Vol, Conf"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Esq, Vol, Conf, Dir"
|
||||||
|
STR_PREV_NEXT: "Ant/Próx"
|
||||||
|
STR_NEXT_PREV: "Próx/Ant"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Pequeno"
|
||||||
|
STR_MEDIUM: "Médio"
|
||||||
|
STR_LARGE: "Grande"
|
||||||
|
STR_X_LARGE: "Extra grande"
|
||||||
|
STR_TIGHT: "Apertado"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Largo"
|
||||||
|
STR_JUSTIFY: "Justificar"
|
||||||
|
STR_ALIGN_LEFT: "Esquerda"
|
||||||
|
STR_CENTER: "Centralizar"
|
||||||
|
STR_ALIGN_RIGHT: "Direita"
|
||||||
|
STR_MIN_1: "1 min"
|
||||||
|
STR_MIN_5: "5 min"
|
||||||
|
STR_MIN_10: "10 min"
|
||||||
|
STR_MIN_15: "15 min"
|
||||||
|
STR_MIN_30: "30 min"
|
||||||
|
STR_PAGES_1: "1 página"
|
||||||
|
STR_PAGES_5: "5 páginas"
|
||||||
|
STR_PAGES_10: "10 páginas"
|
||||||
|
STR_PAGES_15: "15 páginas"
|
||||||
|
STR_PAGES_30: "30 páginas"
|
||||||
|
STR_UPDATE: "Atualizar"
|
||||||
|
STR_CHECKING_UPDATE: "Verificando atualização..."
|
||||||
|
STR_NEW_UPDATE: "Nova atualização disponível!"
|
||||||
|
STR_CURRENT_VERSION: "Versão atual:"
|
||||||
|
STR_NEW_VERSION: "Nova versão:"
|
||||||
|
STR_UPDATING: "Atualizando..."
|
||||||
|
STR_NO_UPDATE: "Nenhuma atualização disponível"
|
||||||
|
STR_UPDATE_FAILED: "Falha na atualização"
|
||||||
|
STR_UPDATE_COMPLETE: "Atualização concluída"
|
||||||
|
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente"
|
||||||
|
STR_EXTERNAL_FONT: "Fonte externa"
|
||||||
|
STR_BUILTIN_DISABLED: "Integrada (desativada)"
|
||||||
|
STR_NO_ENTRIES: "Nenhum entries encontrado"
|
||||||
|
STR_DOWNLOADING: "Baixando..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Falha no download"
|
||||||
|
STR_ERROR_MSG: "Erro:"
|
||||||
|
STR_UNNAMED: "Sem nome"
|
||||||
|
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
|
||||||
|
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
|
||||||
|
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
|
||||||
|
STR_NETWORK_PREFIX: "Rede:"
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Endereço IP:"
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "ou escaneie o QR code com seu celular conectar ao Wi‑Fi."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
|
||||||
|
STR_SD_CARD: "Cartão SD"
|
||||||
|
STR_BACK: "« Voltar"
|
||||||
|
STR_EXIT: "« Sair"
|
||||||
|
STR_HOME: "« Início"
|
||||||
|
STR_SAVE: "« Salvar"
|
||||||
|
STR_SELECT: "Escolher"
|
||||||
|
STR_TOGGLE: "Alternar"
|
||||||
|
STR_CONFIRM: "Confirmar"
|
||||||
|
STR_CANCEL: "Cancelar"
|
||||||
|
STR_CONNECT: "Conectar"
|
||||||
|
STR_OPEN: "Abrir"
|
||||||
|
STR_DOWNLOAD: "Baixar"
|
||||||
|
STR_RETRY: "Tentar novamente"
|
||||||
|
STR_YES: "Sim"
|
||||||
|
STR_NO: "Não"
|
||||||
|
STR_STATE_ON: "LIG."
|
||||||
|
STR_STATE_OFF: "DESL."
|
||||||
|
STR_SET: "Definir"
|
||||||
|
STR_NOT_SET: "Não definido"
|
||||||
|
STR_DIR_LEFT: "Esquerda"
|
||||||
|
STR_DIR_RIGHT: "Direita"
|
||||||
|
STR_DIR_UP: "Cima"
|
||||||
|
STR_DIR_DOWN: "Baixo"
|
||||||
|
STR_CAPS_ON: "CAPS"
|
||||||
|
STR_CAPS_OFF: "caps"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[LIGADO]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
|
||||||
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Completa c/ porcentagem"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Completa c/ barra livro"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Só barra do livro"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo"
|
||||||
|
STR_UI_THEME: "Tema da interface"
|
||||||
|
STR_THEME_CLASSIC: "Clássico"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
|
||||||
|
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||||
|
STR_COVER_CUSTOM: "Capa + personalizado"
|
||||||
|
STR_RECENTS: "Recentes"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||||
|
STR_NO_RECENT_BOOKS: "Sem livros recentes"
|
||||||
|
STR_CALIBRE_DESC: "Usar transferências sem fio Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Esquecer a rede e remover a senha salva?"
|
||||||
|
STR_FORGET_BUTTON: "Esquecer rede"
|
||||||
|
STR_CALIBRE_STARTING: "Iniciando Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Configuração"
|
||||||
|
STR_CALIBRE_STATUS: "Status"
|
||||||
|
STR_CLEAR_BUTTON: "Limpar"
|
||||||
|
STR_DEFAULT_VALUE: "Padrão"
|
||||||
|
STR_REMAP_PROMPT: "Pressione um botão frontal cada função"
|
||||||
|
STR_UNASSIGNED: "Não atribuído"
|
||||||
|
STR_ALREADY_ASSIGNED: "Já atribuído"
|
||||||
|
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento"
|
||||||
|
STR_HW_BACK_LABEL: "Voltar (1º botão)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
|
||||||
|
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
|
||||||
|
STR_GO_TO_PERCENT: "Ir para %"
|
||||||
|
STR_GO_HOME_BUTTON: "Ir para o início"
|
||||||
|
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||||
|
STR_DELETE_CACHE: "Excluir cache do livro"
|
||||||
|
STR_CHAPTER_PREFIX: "Capítulo:"
|
||||||
|
STR_PAGES_SEPARATOR: "páginas |"
|
||||||
|
STR_BOOK_PREFIX: "Livro:"
|
||||||
|
STR_KBD_SHIFT: "shift"
|
||||||
|
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||||
|
STR_KBD_LOCK: "TRAVAR"
|
||||||
|
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
|
||||||
|
STR_PERCENT_STEP_HINT: "Esq/Dir: 1% Cima/Baixo: 10%"
|
||||||
|
STR_SYNCING_TIME: "Sincronizando horário..."
|
||||||
|
STR_CALC_HASH: "Calculando hash documento..."
|
||||||
|
STR_HASH_FAILED: "Falha ao calcular o hash documento"
|
||||||
|
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Enviando progresso..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader em Config."
|
||||||
|
STR_PROGRESS_FOUND: "Progresso encontrado!"
|
||||||
|
STR_REMOTE_LABEL: "Remoto:"
|
||||||
|
STR_LOCAL_LABEL: "Local:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
|
||||||
|
STR_DEVICE_FROM_FORMAT: "De: %s"
|
||||||
|
STR_APPLY_REMOTE: "Aplicar progresso remoto"
|
||||||
|
STR_UPLOAD_LOCAL: "Enviar progresso local"
|
||||||
|
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
|
||||||
|
STR_UPLOAD_PROMPT: "Enviar posição atual?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Progresso enviado!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Falha na sincronização"
|
||||||
|
STR_SECTION_PREFIX: "Seção"
|
||||||
|
STR_UPLOAD: "Enviar"
|
||||||
|
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||||
|
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||||
|
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||||
317
lib/I18n/translations/russian.yaml
Normal file
317
lib/I18n/translations/russian.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Русский"
|
||||||
|
_language_code: "RUSSIAN"
|
||||||
|
_order: "6"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "Загрузка"
|
||||||
|
STR_SLEEPING: "Спящий режим"
|
||||||
|
STR_ENTERING_SLEEP: "Переход в сон..."
|
||||||
|
STR_BROWSE_FILES: "Обзор файлов"
|
||||||
|
STR_FILE_TRANSFER: "Передача файлов"
|
||||||
|
STR_SETTINGS_TITLE: "Настройки"
|
||||||
|
STR_CALIBRE_LIBRARY: "Библиотека Calibre"
|
||||||
|
STR_CONTINUE_READING: "Продолжить чтение"
|
||||||
|
STR_NO_OPEN_BOOK: "Нет открытой книги"
|
||||||
|
STR_START_READING: "Начать чтение ниже"
|
||||||
|
STR_BOOKS: "Книги"
|
||||||
|
STR_NO_BOOKS_FOUND: "Книги не найдены"
|
||||||
|
STR_SELECT_CHAPTER: "Выберите главу"
|
||||||
|
STR_NO_CHAPTERS: "Глав нет"
|
||||||
|
STR_END_OF_BOOK: "Конец книги"
|
||||||
|
STR_EMPTY_CHAPTER: "Пустая глава"
|
||||||
|
STR_INDEXING: "Индексация..."
|
||||||
|
STR_MEMORY_ERROR: "Ошибка памяти"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Ошибка загрузки страницы"
|
||||||
|
STR_EMPTY_FILE: "Пустой файл"
|
||||||
|
STR_OUT_OF_BOUNDS: "Выход за пределы"
|
||||||
|
STR_LOADING: "Загрузка..."
|
||||||
|
STR_LOAD_XTC_FAILED: "Не удалось загрузить XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Не удалось загрузить TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Не удалось загрузить EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "Ошибка SD-карты"
|
||||||
|
STR_WIFI_NETWORKS: "Wi-Fi сети"
|
||||||
|
STR_NO_NETWORKS: "Сети не найдены"
|
||||||
|
STR_NETWORKS_FOUND: "Найдено сетей: %zu"
|
||||||
|
STR_SCANNING: "Сканирование..."
|
||||||
|
STR_CONNECTING: "Подключение..."
|
||||||
|
STR_CONNECTED: "Подключено!"
|
||||||
|
STR_CONNECTION_FAILED: "Ошибка подключения"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Тайм-аут подключения"
|
||||||
|
STR_FORGET_NETWORK: "Забыть сеть?"
|
||||||
|
STR_SAVE_PASSWORD: "Сохранить пароль?"
|
||||||
|
STR_REMOVE_PASSWORD: "Удалить сохранённый пароль?"
|
||||||
|
STR_PRESS_OK_SCAN: "Нажмите OK для повторного поиска"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Нажмите любую кнопку"
|
||||||
|
STR_SELECT_HINT: "ВЛЕВО/ВПРАВО: выбор | OK: подтвердить"
|
||||||
|
STR_HOW_CONNECT: "Как вы хотите подключиться?"
|
||||||
|
STR_JOIN_NETWORK: "Подключиться к сети"
|
||||||
|
STR_CREATE_HOTSPOT: "Создать точку доступа"
|
||||||
|
STR_JOIN_DESC: "Подключение к существующей сети Wi-Fi"
|
||||||
|
STR_HOTSPOT_DESC: "Создать сеть Wi-Fi для подключения других"
|
||||||
|
STR_STARTING_HOTSPOT: "Запуск точки доступа..."
|
||||||
|
STR_HOTSPOT_MODE: "Режим точки доступа"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Подключите устройство к этой сети Wi-Fi"
|
||||||
|
STR_OPEN_URL_HINT: "Откройте этот адрес в браузере"
|
||||||
|
STR_OR_HTTP_PREFIX: "или http://"
|
||||||
|
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
|
||||||
|
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
|
||||||
|
STR_CONNECT_WIRELESS: "Подключить как беспроводное устройство"
|
||||||
|
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
|
||||||
|
STR_MAC_ADDRESS: "MAC-адрес:"
|
||||||
|
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Введите пароль Wi-Fi"
|
||||||
|
STR_ENTER_TEXT: "Введите текст"
|
||||||
|
STR_TO_PREFIX: "к "
|
||||||
|
STR_CALIBRE_DISCOVERING: "Поиск Calibre..."
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Подключение к "
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Подключено к "
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Ожидание команд..."
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Ошибка подключения"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Соединение с Calibre разорвано"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Ожидание передачи..."
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Если передача не удаётся"
|
||||||
|
STR_CALIBRE_RECEIVING: "Получение:"
|
||||||
|
STR_CALIBRE_RECEIVED: "Получено:"
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Ожидание следующих файлов..."
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Не удалось создать файл"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Требуется пароль"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Передача прервана"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Установите плагин CrossPoint Reader"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Подключитесь к той же сети Wi-Fi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) В Calibre выберите: «Отправить на устройство»"
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "Не закрывайте этот экран во время отправки"
|
||||||
|
STR_CAT_DISPLAY: "Экран"
|
||||||
|
STR_CAT_READER: "Чтение"
|
||||||
|
STR_CAT_CONTROLS: "Управление"
|
||||||
|
STR_CAT_SYSTEM: "Система"
|
||||||
|
STR_SLEEP_SCREEN: "Экран сна"
|
||||||
|
STR_SLEEP_COVER_MODE: "Режим обложки сна"
|
||||||
|
STR_STATUS_BAR: "Строка состояния"
|
||||||
|
STR_HIDE_BATTERY: "Скрыть % батареи"
|
||||||
|
STR_EXTRA_SPACING: "Доп. интервал абзаца"
|
||||||
|
STR_TEXT_AA: "Сглаживание текста"
|
||||||
|
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
|
||||||
|
STR_ORIENTATION: "Ориентация чтения"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Боковые кнопки"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
|
||||||
|
STR_LONG_PRESS_SKIP: "Долгое нажатие - смена главы"
|
||||||
|
STR_FONT_FAMILY: "Шрифт чтения"
|
||||||
|
STR_EXT_READER_FONT: "Внешний шрифт чтения"
|
||||||
|
STR_EXT_CHINESE_FONT: "Шрифт CJK"
|
||||||
|
STR_EXT_UI_FONT: "Шрифт интерфейса"
|
||||||
|
STR_FONT_SIZE: "Размер шрифта интерфейса"
|
||||||
|
STR_LINE_SPACING: "Межстрочный интервал"
|
||||||
|
STR_ASCII_LETTER_SPACING: "Интервал букв ASCII"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "Интервал цифр ASCII"
|
||||||
|
STR_CJK_SPACING: "Интервал CJK"
|
||||||
|
STR_COLOR_MODE: "Цветовой режим"
|
||||||
|
STR_SCREEN_MARGIN: "Поля экрана"
|
||||||
|
STR_PARA_ALIGNMENT: "Выравнивание абзаца"
|
||||||
|
STR_HYPHENATION: "Перенос слов"
|
||||||
|
STR_TIME_TO_SLEEP: "Сон через"
|
||||||
|
STR_REFRESH_FREQ: "Частота обновления"
|
||||||
|
STR_CALIBRE_SETTINGS: "Настройки Calibre"
|
||||||
|
STR_KOREADER_SYNC: "Синхронизация KOReader"
|
||||||
|
STR_CHECK_UPDATES: "Проверить обновления"
|
||||||
|
STR_LANGUAGE: "Язык"
|
||||||
|
STR_SELECT_WALLPAPER: "Выбрать обои"
|
||||||
|
STR_CLEAR_READING_CACHE: "Очистить кэш чтения"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Имя пользователя"
|
||||||
|
STR_PASSWORD: "Пароль"
|
||||||
|
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
||||||
|
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
||||||
|
STR_AUTHENTICATE: "Авторизация"
|
||||||
|
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
||||||
|
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||||
|
STR_FILENAME: "Имя файла"
|
||||||
|
STR_BINARY: "Бинарный"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Сначала укажите данные"
|
||||||
|
STR_WIFI_CONN_FAILED: "Не удалось подключиться к Wi-Fi"
|
||||||
|
STR_AUTHENTICATING: "Авторизация..."
|
||||||
|
STR_AUTH_SUCCESS: "Авторизация успешна!"
|
||||||
|
STR_KOREADER_AUTH: "Авторизация KOReader"
|
||||||
|
STR_SYNC_READY: "Синхронизация KOReader готова"
|
||||||
|
STR_AUTH_FAILED: "Ошибка авторизации"
|
||||||
|
STR_DONE: "Готово"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Будут удалены все данные кэша книг."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Весь прогресс чтения будет потерян!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Книги потребуется переиндексировать"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "при повторном открытии."
|
||||||
|
STR_CLEARING_CACHE: "Очистка кэша..."
|
||||||
|
STR_CACHE_CLEARED: "Кэш очищен"
|
||||||
|
STR_ITEMS_REMOVED: "элементов удалено"
|
||||||
|
STR_FAILED_LOWER: "ошибка"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Не удалось очистить кэш"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Проверьте вывод по UART для деталей"
|
||||||
|
STR_DARK: "Тёмный"
|
||||||
|
STR_LIGHT: "Светлый"
|
||||||
|
STR_CUSTOM: "Свой"
|
||||||
|
STR_COVER: "Обложка"
|
||||||
|
STR_NONE_OPT: "Нет"
|
||||||
|
STR_FIT: "Вписать"
|
||||||
|
STR_CROP: "Обрезать"
|
||||||
|
STR_NO_PROGRESS: "Без прогресса"
|
||||||
|
STR_FULL_OPT: "Полная"
|
||||||
|
STR_NEVER: "Никогда"
|
||||||
|
STR_IN_READER: "В режиме чтения"
|
||||||
|
STR_ALWAYS: "Всегда"
|
||||||
|
STR_IGNORE: "Игнорировать"
|
||||||
|
STR_SLEEP: "Сон"
|
||||||
|
STR_PAGE_TURN: "Перелистывание"
|
||||||
|
STR_PORTRAIT: "Портрет"
|
||||||
|
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||||
|
STR_INVERTED: "Инверсия"
|
||||||
|
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Наз, Ок, Лев, Прав"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Лев, Прав, Наз, Ок"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Лев, Наз, Ок, Прав"
|
||||||
|
STR_PREV_NEXT: "Назад/Вперёд"
|
||||||
|
STR_NEXT_PREV: "Вперёд/Назад"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Маленький"
|
||||||
|
STR_MEDIUM: "Средний"
|
||||||
|
STR_LARGE: "Большой"
|
||||||
|
STR_X_LARGE: "Очень большой"
|
||||||
|
STR_TIGHT: "Узкий"
|
||||||
|
STR_NORMAL: "Обычный"
|
||||||
|
STR_WIDE: "Широкий"
|
||||||
|
STR_JUSTIFY: "По ширине"
|
||||||
|
STR_ALIGN_LEFT: "По левому краю"
|
||||||
|
STR_CENTER: "По центру"
|
||||||
|
STR_ALIGN_RIGHT: "По правому краю"
|
||||||
|
STR_MIN_1: "1 мин"
|
||||||
|
STR_MIN_5: "5 мин"
|
||||||
|
STR_MIN_10: "10 мин"
|
||||||
|
STR_MIN_15: "15 мин"
|
||||||
|
STR_MIN_30: "30 мин"
|
||||||
|
STR_PAGES_1: "1 стр."
|
||||||
|
STR_PAGES_5: "5 стр."
|
||||||
|
STR_PAGES_10: "10 стр."
|
||||||
|
STR_PAGES_15: "15 стр."
|
||||||
|
STR_PAGES_30: "30 стр."
|
||||||
|
STR_UPDATE: "Обновление"
|
||||||
|
STR_CHECKING_UPDATE: "Проверка обновлений..."
|
||||||
|
STR_NEW_UPDATE: "Доступно новое обновление!"
|
||||||
|
STR_CURRENT_VERSION: "Текущая версия:"
|
||||||
|
STR_NEW_VERSION: "Новая версия:"
|
||||||
|
STR_UPDATING: "Обновление..."
|
||||||
|
STR_NO_UPDATE: "Обновлений нет"
|
||||||
|
STR_UPDATE_FAILED: "Ошибка обновления"
|
||||||
|
STR_UPDATE_COMPLETE: "Обновление завершено"
|
||||||
|
STR_POWER_ON_HINT: "Удерживайте кнопку питания для включения"
|
||||||
|
STR_EXTERNAL_FONT: "Пользовательский шрифт"
|
||||||
|
STR_BUILTIN_DISABLED: "Встроенный (отключён)"
|
||||||
|
STR_NO_ENTRIES: "Записи не найдены"
|
||||||
|
STR_DOWNLOADING: "Загрузка..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Ошибка загрузки"
|
||||||
|
STR_ERROR_MSG: "Ошибка:"
|
||||||
|
STR_UNNAMED: "Без имени"
|
||||||
|
STR_NO_SERVER_URL: "URL сервера не настроен"
|
||||||
|
STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
|
||||||
|
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
|
||||||
|
STR_NETWORK_PREFIX: "Сеть:"
|
||||||
|
STR_IP_ADDRESS_PREFIX: "IP-адрес:"
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "или отсканируйте QR-код для подключения к Wi-Fi."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Ошибка: Общая ошибка"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Ошибка: Сеть не найдена"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Ошибка: Тайм-аут соединения"
|
||||||
|
STR_SD_CARD: "SD-карта"
|
||||||
|
STR_BACK: "« Назад"
|
||||||
|
STR_EXIT: "« Выход"
|
||||||
|
STR_HOME: "« Главная"
|
||||||
|
STR_SAVE: "« Сохранить"
|
||||||
|
STR_SELECT: "Выбрать"
|
||||||
|
STR_TOGGLE: "Выбор"
|
||||||
|
STR_CONFIRM: "Подтв."
|
||||||
|
STR_CANCEL: "Отмена"
|
||||||
|
STR_CONNECT: "Подкл."
|
||||||
|
STR_OPEN: "Открыть"
|
||||||
|
STR_DOWNLOAD: "Скачать"
|
||||||
|
STR_RETRY: "Повторить"
|
||||||
|
STR_YES: "Да"
|
||||||
|
STR_NO: "Нет"
|
||||||
|
STR_STATE_ON: "ВКЛ"
|
||||||
|
STR_STATE_OFF: "ВЫКЛ"
|
||||||
|
STR_SET: "Установлено"
|
||||||
|
STR_NOT_SET: "Не установлено"
|
||||||
|
STR_DIR_LEFT: "Влево"
|
||||||
|
STR_DIR_RIGHT: "Вправо"
|
||||||
|
STR_DIR_UP: "Вверх"
|
||||||
|
STR_DIR_DOWN: "Вниз"
|
||||||
|
STR_CAPS_ON: "CAPS"
|
||||||
|
STR_CAPS_OFF: "caps"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[ВКЛ]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Фильтр экрана сна"
|
||||||
|
STR_FILTER_CONTRAST: "Контраст"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Полная + %"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Полная + шкала книги"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Только шкала книги"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы"
|
||||||
|
STR_UI_THEME: "Тема интерфейса"
|
||||||
|
STR_THEME_CLASSIC: "Классическая"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
|
||||||
|
STR_OPDS_BROWSER: "OPDS браузер"
|
||||||
|
STR_COVER_CUSTOM: "Обложка + Свой"
|
||||||
|
STR_RECENTS: "Недавние"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Недавние книги"
|
||||||
|
STR_NO_RECENT_BOOKS: "Нет недавних книг"
|
||||||
|
STR_CALIBRE_DESC: "Использовать беспроводную передачу Calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Забыть сеть и удалить сохранённый пароль?"
|
||||||
|
STR_FORGET_BUTTON: "Забыть сеть"
|
||||||
|
STR_CALIBRE_STARTING: "Запуск Calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Настройка"
|
||||||
|
STR_CALIBRE_STATUS: "Статус"
|
||||||
|
STR_CLEAR_BUTTON: "Очистить"
|
||||||
|
STR_DEFAULT_VALUE: "По умолчанию"
|
||||||
|
STR_REMAP_PROMPT: "Назначьте роль для каждой кнопки"
|
||||||
|
STR_UNASSIGNED: "Не назначено"
|
||||||
|
STR_ALREADY_ASSIGNED: "Уже назначено"
|
||||||
|
STR_REMAP_RESET_HINT: "Боковая кнопка вверх: сбросить по умолчанию"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Боковая кнопка вниз: отменить переназначение"
|
||||||
|
STR_HW_BACK_LABEL: "Назад (1-я кнопка)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Подтвердить (2-я кнопка)"
|
||||||
|
STR_HW_LEFT_LABEL: "Влево (3-я кнопка)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Вправо (4-я кнопка)"
|
||||||
|
STR_GO_TO_PERCENT: "Перейти к %"
|
||||||
|
STR_GO_HOME_BUTTON: "На главную"
|
||||||
|
STR_SYNC_PROGRESS: "Синхронизировать прогресс"
|
||||||
|
STR_DELETE_CACHE: "Удалить кэш книги"
|
||||||
|
STR_CHAPTER_PREFIX: "Глава:"
|
||||||
|
STR_PAGES_SEPARATOR: "стр. |"
|
||||||
|
STR_BOOK_PREFIX: "Книга:"
|
||||||
|
STR_KBD_SHIFT: "shift"
|
||||||
|
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||||
|
STR_KBD_LOCK: "LOCK"
|
||||||
|
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
|
||||||
|
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
|
||||||
|
STR_SYNCING_TIME: "Синхронизация времени..."
|
||||||
|
STR_CALC_HASH: "Расчёт хэша документа..."
|
||||||
|
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
|
||||||
|
STR_FETCH_PROGRESS: "Получение удалённого прогресса..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Отправка прогресса..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Данные для входа не настроены"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Настройте аккаунт KOReader в настройках"
|
||||||
|
STR_PROGRESS_FOUND: "Прогресс найден!"
|
||||||
|
STR_REMOTE_LABEL: "Удалённый:"
|
||||||
|
STR_LOCAL_LABEL: "Локальный:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Страница %d, %.2f%% всего"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Страница %d/%d"
|
||||||
|
STR_DEVICE_FROM_FORMAT: "От: %s"
|
||||||
|
STR_APPLY_REMOTE: "Применить удалённый прогресс"
|
||||||
|
STR_UPLOAD_LOCAL: "Отправить локальный прогресс"
|
||||||
|
STR_NO_REMOTE_MSG: "Удалённый прогресс не найден"
|
||||||
|
STR_UPLOAD_PROMPT: "Отправить текущую позицию?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Прогресс отправлен!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Ошибка синхронизации"
|
||||||
|
STR_SECTION_PREFIX: "Раздел"
|
||||||
|
STR_UPLOAD: "Отправить"
|
||||||
|
STR_BOOK_S_STYLE: "Стиль книги"
|
||||||
|
STR_EMBEDDED_STYLE: "Встроенный стиль"
|
||||||
|
STR_OPDS_SERVER_URL: "URL OPDS сервера"
|
||||||
317
lib/I18n/translations/spanish.yaml
Normal file
317
lib/I18n/translations/spanish.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Español"
|
||||||
|
_language_code: "SPANISH"
|
||||||
|
_order: "1"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
|
STR_BOOTING: "BOOTING"
|
||||||
|
STR_SLEEPING: "SLEEPING"
|
||||||
|
STR_ENTERING_SLEEP: "ENTERING SLEEP..."
|
||||||
|
STR_BROWSE_FILES: "Buscar archivos"
|
||||||
|
STR_FILE_TRANSFER: "Transferencia de archivos"
|
||||||
|
STR_SETTINGS_TITLE: "Configuración"
|
||||||
|
STR_CALIBRE_LIBRARY: "Libreria Calibre"
|
||||||
|
STR_CONTINUE_READING: "Continuar leyendo"
|
||||||
|
STR_NO_OPEN_BOOK: "No hay libros abiertos"
|
||||||
|
STR_START_READING: "Start reading below"
|
||||||
|
STR_BOOKS: "Libros"
|
||||||
|
STR_NO_BOOKS_FOUND: "No se encontraron libros"
|
||||||
|
STR_SELECT_CHAPTER: "Seleccionar capítulo"
|
||||||
|
STR_NO_CHAPTERS: "Sin capítulos"
|
||||||
|
STR_END_OF_BOOK: "Fin del libro"
|
||||||
|
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
||||||
|
STR_INDEXING: "Indexando..."
|
||||||
|
STR_MEMORY_ERROR: "Error de memoria"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
||||||
|
STR_EMPTY_FILE: "Archivo vacío"
|
||||||
|
STR_OUT_OF_BOUNDS: "Out of bounds"
|
||||||
|
STR_LOADING: "Cargando..."
|
||||||
|
STR_LOAD_XTC_FAILED: "Error al cargar XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Error al cargar TXT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Error al cargar EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "Error en la tarjeta SD"
|
||||||
|
STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||||
|
STR_NO_NETWORKS: "No hay redes disponibles"
|
||||||
|
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||||
|
STR_SCANNING: "Buscando..."
|
||||||
|
STR_CONNECTING: "Conectando..."
|
||||||
|
STR_CONNECTED: "Conectado!"
|
||||||
|
STR_CONNECTION_FAILED: "Error de conexion"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Connection timeout"
|
||||||
|
STR_FORGET_NETWORK: "Olvidar la red?"
|
||||||
|
STR_SAVE_PASSWORD: "Guardar contraseña para la próxima vez?"
|
||||||
|
STR_REMOVE_PASSWORD: "Borrar contraseñas guardadas?"
|
||||||
|
STR_PRESS_OK_SCAN: "Presione OK para buscar de nuevo"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Presione cualquier botón para continuar"
|
||||||
|
STR_SELECT_HINT: "Izquierda/Derecha: Seleccionar | OK: Confirmar"
|
||||||
|
STR_HOW_CONNECT: "Cómo te gustaría conectarte?"
|
||||||
|
STR_JOIN_NETWORK: "Unirse a una red"
|
||||||
|
STR_CREATE_HOTSPOT: "Crear punto de acceso"
|
||||||
|
STR_JOIN_DESC: "Conectarse a una red Wi-Fi existente"
|
||||||
|
STR_HOTSPOT_DESC: "Crear una red Wi-Fi para que otros se unan"
|
||||||
|
STR_STARTING_HOTSPOT: "Iniciando punto de acceso..."
|
||||||
|
STR_HOTSPOT_MODE: "Modo punto de acceso"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Conectar su dispositivo a esta red Wi-Fi"
|
||||||
|
STR_OPEN_URL_HINT: "Abre esta dirección en tu navegador"
|
||||||
|
STR_OR_HTTP_PREFIX: "o http://"
|
||||||
|
STR_SCAN_QR_HINT: "o escanee este código QR con su móvil:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre inalámbrico"
|
||||||
|
STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre"
|
||||||
|
STR_CONNECT_WIRELESS: "Conectar como dispositivo inalámbrico"
|
||||||
|
STR_NETWORK_LEGEND: "* = Cifrado | + = Guardado"
|
||||||
|
STR_MAC_ADDRESS: "Dirección MAC:"
|
||||||
|
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Introduzca la contraseña de Wi-Fi"
|
||||||
|
STR_ENTER_TEXT: "Introduzca el texto"
|
||||||
|
STR_TO_PREFIX: "a "
|
||||||
|
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Conectándose a"
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Conectado a "
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Esperando comandos..."
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Error de conexión, intentándolo nuevamente)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Esperando transferencia..."
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Si la transferencia falla, habilite \\n'Ignorar espacio libre' en las configuraciones del \\nplugin smartdevice de calibre."
|
||||||
|
STR_CALIBRE_RECEIVING: "Recibiendo: "
|
||||||
|
STR_CALIBRE_RECEIVED: "Recibido: "
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Esperando más..."
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Error al crear el archivo"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Contraseña requerida"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transferencia interrumpida"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Instala CrossPoint Reader plugin"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Conéctese a la misma red Wi-Fi"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) En Calibre: \"Enviar a dispotivo\""
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "\"Permanezca en esta pantalla mientras se envía\""
|
||||||
|
STR_CAT_DISPLAY: "Pantalla"
|
||||||
|
STR_CAT_READER: "Lector"
|
||||||
|
STR_CAT_CONTROLS: "Control"
|
||||||
|
STR_CAT_SYSTEM: "Sistema"
|
||||||
|
STR_SLEEP_SCREEN: "Salva Pantallas"
|
||||||
|
STR_SLEEP_COVER_MODE: "Modo de salva pantallas"
|
||||||
|
STR_STATUS_BAR: "Barra de estado"
|
||||||
|
STR_HIDE_BATTERY: "Ocultar porcentaje de batería"
|
||||||
|
STR_EXTRA_SPACING: "Espaciado extra de párrafos"
|
||||||
|
STR_TEXT_AA: "Suavizado de bordes de texto"
|
||||||
|
STR_SHORT_PWR_BTN: "Clic breve del botón de encendido"
|
||||||
|
STR_ORIENTATION: "Orientación de la lectura"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Diseño de los botones frontales"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Diseño de los botones laterales (Lector)"
|
||||||
|
STR_LONG_PRESS_SKIP: "Pasar a la capítulo al presiónar largamente"
|
||||||
|
STR_FONT_FAMILY: "Familia de tipografía del lector"
|
||||||
|
STR_EXT_READER_FONT: "Tipografía externa"
|
||||||
|
STR_EXT_CHINESE_FONT: "Tipografía (Lectura)"
|
||||||
|
STR_EXT_UI_FONT: "Tipografía (Pantalla)"
|
||||||
|
STR_FONT_SIZE: "Tamaño de la fuente (Pantalla)"
|
||||||
|
STR_LINE_SPACING: "Interlineado (Lectura)"
|
||||||
|
STR_ASCII_LETTER_SPACING: "Espaciado de letras ASCII"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "Espaciado de dígitos ASCII"
|
||||||
|
STR_CJK_SPACING: "Espaciado CJK"
|
||||||
|
STR_COLOR_MODE: "Modo de color"
|
||||||
|
STR_SCREEN_MARGIN: "Margen de lectura"
|
||||||
|
STR_PARA_ALIGNMENT: "Ajuste de parágrafo del lector"
|
||||||
|
STR_HYPHENATION: "Hyphenation"
|
||||||
|
STR_TIME_TO_SLEEP: "Tiempo para dormir"
|
||||||
|
STR_REFRESH_FREQ: "Frecuencia de actualización"
|
||||||
|
STR_CALIBRE_SETTINGS: "Configuraciones de Calibre"
|
||||||
|
STR_KOREADER_SYNC: "Síncronización de KOReader"
|
||||||
|
STR_CHECK_UPDATES: "Verificar actualizaciones"
|
||||||
|
STR_LANGUAGE: "Idioma"
|
||||||
|
STR_SELECT_WALLPAPER: "Seleccionar fondo"
|
||||||
|
STR_CLEAR_READING_CACHE: "Borrar caché de lectura"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Nombre de usuario"
|
||||||
|
STR_PASSWORD: "Contraseña"
|
||||||
|
STR_SYNC_SERVER_URL: "URL del servidor de síncronización"
|
||||||
|
STR_DOCUMENT_MATCHING: "Coincidencia de documentos"
|
||||||
|
STR_AUTHENTICATE: "Autentificar"
|
||||||
|
STR_KOREADER_USERNAME: "Nombre de usuario de KOReader"
|
||||||
|
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
||||||
|
STR_FILENAME: "Nombre del archivo"
|
||||||
|
STR_BINARY: "Binario"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Configurar credenciales primero"
|
||||||
|
STR_WIFI_CONN_FAILED: "Falló la conexión Wi-Fi"
|
||||||
|
STR_AUTHENTICATING: "Autentificando..."
|
||||||
|
STR_AUTH_SUCCESS: "Autenticación exitsosa!"
|
||||||
|
STR_KOREADER_AUTH: "Autenticación KOReader"
|
||||||
|
STR_SYNC_READY: "La síncronización de KOReader está lista para usarse"
|
||||||
|
STR_AUTH_FAILED: "Falló la autenticación"
|
||||||
|
STR_DONE: "Hecho"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Esto borrará todos los datos en cache del libro."
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: " ¡Se perderá todo el avance de leer!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Los libros deberán ser reíndexados"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "cuando se abran de nuevo."
|
||||||
|
STR_CLEARING_CACHE: "Borrando caché..."
|
||||||
|
STR_CACHE_CLEARED: "Cache limpia"
|
||||||
|
STR_ITEMS_REMOVED: "Elementos eliminados"
|
||||||
|
STR_FAILED_LOWER: "Falló"
|
||||||
|
STR_CLEAR_CACHE_FAILED: "No se pudo borrar la cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Verifique la salida serial para detalles"
|
||||||
|
STR_DARK: "Oscuro"
|
||||||
|
STR_LIGHT: "Claro"
|
||||||
|
STR_CUSTOM: "Personalizado"
|
||||||
|
STR_COVER: "Portada"
|
||||||
|
STR_NONE_OPT: "Ninguno"
|
||||||
|
STR_FIT: "Ajustar"
|
||||||
|
STR_CROP: "Recortar"
|
||||||
|
STR_NO_PROGRESS: "Sin avance"
|
||||||
|
STR_FULL_OPT: "Completa"
|
||||||
|
STR_NEVER: "Nunca"
|
||||||
|
STR_IN_READER: "En el lector"
|
||||||
|
STR_ALWAYS: "Siempre"
|
||||||
|
STR_IGNORE: "Ignorar"
|
||||||
|
STR_SLEEP: "Dormir"
|
||||||
|
STR_PAGE_TURN: "Paso de página"
|
||||||
|
STR_PORTRAIT: "Portrato"
|
||||||
|
STR_LANDSCAPE_CW: "Paisaje sentido horario"
|
||||||
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_LANDSCAPE_CCW: "Paisaje sentido antihorario"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Atrás, Confirmar, Izquierda, Derecha"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Izquierda, Derecha, Atrás, Confirmar"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Izquierda, Atrás, Confirmar, Derecha"
|
||||||
|
STR_PREV_NEXT: "Anterior/Siguiente"
|
||||||
|
STR_NEXT_PREV: "Siguiente/Anterior"
|
||||||
|
STR_BOOKERLY: "Relacionado con libros"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||||
|
STR_SMALL: "Pequeño"
|
||||||
|
STR_MEDIUM: "Medio"
|
||||||
|
STR_LARGE: "Grande"
|
||||||
|
STR_X_LARGE: "Extra grande"
|
||||||
|
STR_TIGHT: "Ajustado"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Ancho"
|
||||||
|
STR_JUSTIFY: "Justificar"
|
||||||
|
STR_ALIGN_LEFT: "Izquierda"
|
||||||
|
STR_CENTER: "Centro"
|
||||||
|
STR_ALIGN_RIGHT: "Derecha"
|
||||||
|
STR_MIN_1: "1 Minuto"
|
||||||
|
STR_MIN_5: "10 Minutos"
|
||||||
|
STR_MIN_10: "5 Minutos"
|
||||||
|
STR_MIN_15: "15 Minutos"
|
||||||
|
STR_MIN_30: "30 Minutos"
|
||||||
|
STR_PAGES_1: "1 Página"
|
||||||
|
STR_PAGES_5: "5 Páginas"
|
||||||
|
STR_PAGES_10: "10 Páginas"
|
||||||
|
STR_PAGES_15: "15 Páginas"
|
||||||
|
STR_PAGES_30: "30 Páginas"
|
||||||
|
STR_UPDATE: "ActualizaR"
|
||||||
|
STR_CHECKING_UPDATE: "Verificando actualización..."
|
||||||
|
STR_NEW_UPDATE: "¡Nueva actualización disponible!"
|
||||||
|
STR_CURRENT_VERSION: "Versión actual:"
|
||||||
|
STR_NEW_VERSION: "Nueva versión:"
|
||||||
|
STR_UPDATING: "Actualizando..."
|
||||||
|
STR_NO_UPDATE: "No hay actualizaciones disponibles"
|
||||||
|
STR_UPDATE_FAILED: "Falló la actualización"
|
||||||
|
STR_UPDATE_COMPLETE: "Actualización completada"
|
||||||
|
STR_POWER_ON_HINT: "Presione y mantenga presionado el botón de encendido para volver a encender"
|
||||||
|
STR_EXTERNAL_FONT: "Fuente externa"
|
||||||
|
STR_BUILTIN_DISABLED: "Incorporado (Desactivado)"
|
||||||
|
STR_NO_ENTRIES: "No se encontraron elementos"
|
||||||
|
STR_DOWNLOADING: "Descargando..."
|
||||||
|
STR_DOWNLOAD_FAILED: "Falló la descarga"
|
||||||
|
STR_ERROR_MSG: "Error"
|
||||||
|
STR_UNNAMED: "Sin nombre"
|
||||||
|
STR_NO_SERVER_URL: "No se ha configurado la url del servidor"
|
||||||
|
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||||
|
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||||
|
STR_NETWORK_PREFIX: "Red: "
|
||||||
|
STR_IP_ADDRESS_PREFIX: "Dirección IP: "
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "O escanee el código QR con su teléfono para conectarse a WI-FI."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Error: Fallo general"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Error: Red no encontrada"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
|
||||||
|
STR_SD_CARD: "Tarjeta SD"
|
||||||
|
STR_BACK: "« Atrás"
|
||||||
|
STR_EXIT: "« SaliR"
|
||||||
|
STR_HOME: "« Inicio"
|
||||||
|
STR_SAVE: "« Guardar"
|
||||||
|
STR_SELECT: "Seleccionar"
|
||||||
|
STR_TOGGLE: "Cambiar"
|
||||||
|
STR_CONFIRM: "Confirmar"
|
||||||
|
STR_CANCEL: "Cancelar"
|
||||||
|
STR_CONNECT: "Conectar"
|
||||||
|
STR_OPEN: "Abrir"
|
||||||
|
STR_DOWNLOAD: "Descargar"
|
||||||
|
STR_RETRY: "Reintentar"
|
||||||
|
STR_YES: "Sí"
|
||||||
|
STR_NO: "No"
|
||||||
|
STR_STATE_ON: "ENCENDIDO"
|
||||||
|
STR_STATE_OFF: "APAGADO"
|
||||||
|
STR_SET: "Configurar"
|
||||||
|
STR_NOT_SET: "No configurado"
|
||||||
|
STR_DIR_LEFT: "Izquierda"
|
||||||
|
STR_DIR_RIGHT: "Derecha"
|
||||||
|
STR_DIR_UP: "Arriba"
|
||||||
|
STR_DIR_DOWN: "Abajo"
|
||||||
|
STR_CAPS_ON: "MAYÚSCULAS"
|
||||||
|
STR_CAPS_OFF: "caps"
|
||||||
|
STR_OK_BUTTON: "OK"
|
||||||
|
STR_ON_MARKER: "[ENCENDIDO]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Filtro de salva pantalla y protección de la pantalla"
|
||||||
|
STR_FILTER_CONTRAST: "Contraste"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Completa con porcentaje"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Completa con progreso del libro"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos"
|
||||||
|
STR_UI_THEME: "Estilo de pantalla"
|
||||||
|
STR_THEME_CLASSIC: "Clásico"
|
||||||
|
STR_THEME_LYRA: "LYRA"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
|
||||||
|
STR_OPDS_BROWSER: "Navegador opds"
|
||||||
|
STR_COVER_CUSTOM: "Portada + Personalizado"
|
||||||
|
STR_RECENTS: "Recientes"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Libros recientes"
|
||||||
|
STR_NO_RECENT_BOOKS: "No hay libros recientes"
|
||||||
|
STR_CALIBRE_DESC: "Utilice las transferencias dispositivos inalámbricos de calibre"
|
||||||
|
STR_FORGET_AND_REMOVE: "Olvidar la red y eliminar la contraseña guardada?"
|
||||||
|
STR_FORGET_BUTTON: "Olvidar la red"
|
||||||
|
STR_CALIBRE_STARTING: "Iniciando calibre..."
|
||||||
|
STR_CALIBRE_SETUP: "Configuración"
|
||||||
|
STR_CALIBRE_STATUS: "Estado"
|
||||||
|
STR_CLEAR_BUTTON: "Borrar"
|
||||||
|
STR_DEFAULT_VALUE: "Previo"
|
||||||
|
STR_REMAP_PROMPT: "Presione un botón frontal para cada función"
|
||||||
|
STR_UNASSIGNED: "No asignado"
|
||||||
|
STR_ALREADY_ASSIGNED: "Ya asignado"
|
||||||
|
STR_REMAP_RESET_HINT: "Botón lateral arriba: Restablecer a la configuración previo"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Botón lateral abajo: Anular reconfiguración"
|
||||||
|
STR_HW_BACK_LABEL: "Atrás (Primer botón)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Confirmar (Segundo botón)"
|
||||||
|
STR_HW_LEFT_LABEL: "Izquierda (Tercer botón)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Derecha (Cuarto botón)"
|
||||||
|
STR_GO_TO_PERCENT: "Ir a %"
|
||||||
|
STR_GO_HOME_BUTTON: "Volver a inicio"
|
||||||
|
STR_SYNC_PROGRESS: "Progreso de síncronización"
|
||||||
|
STR_DELETE_CACHE: "Borrar cache del libro"
|
||||||
|
STR_CHAPTER_PREFIX: "Capítulo:"
|
||||||
|
STR_PAGES_SEPARATOR: " Páginas |"
|
||||||
|
STR_BOOK_PREFIX: "Libro:"
|
||||||
|
STR_KBD_SHIFT: "shift"
|
||||||
|
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||||
|
STR_KBD_LOCK: "BLOQUEAR"
|
||||||
|
STR_CALIBRE_URL_HINT: "Para calibre, agregue /opds a su urL"
|
||||||
|
STR_PERCENT_STEP_HINT: "Izquierda/Derecha: 1% Arriba/Abajo: 10%"
|
||||||
|
STR_SYNCING_TIME: "Tiempo de síncronización..."
|
||||||
|
STR_CALC_HASH: "Calculando hash del documento..."
|
||||||
|
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
||||||
|
STR_FETCH_PROGRESS: "Recuperando progreso remoto..."
|
||||||
|
STR_UPLOAD_PROGRESS: "Subiendo progreso..."
|
||||||
|
STR_NO_CREDENTIALS_MSG: "No se han configurado credenciales"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Configure una cuenta de KOReader en la configuración"
|
||||||
|
STR_PROGRESS_FOUND: "¡Progreso encontrado!"
|
||||||
|
STR_REMOTE_LABEL: "Remoto"
|
||||||
|
STR_LOCAL_LABEL: "Local"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% Completada"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d / %d, %.2f% Completada"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||||
|
STR_APPLY_REMOTE: "Aplicar progreso remoto"
|
||||||
|
STR_UPLOAD_LOCAL: "Subir progreso local"
|
||||||
|
STR_NO_REMOTE_MSG: "No se encontró progreso remoto"
|
||||||
|
STR_UPLOAD_PROMPT: "Subir posicion actual?"
|
||||||
|
STR_UPLOAD_SUCCESS: "¡Progreso subido!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Fallo de síncronización"
|
||||||
|
STR_SECTION_PREFIX: "Seccion"
|
||||||
|
STR_UPLOAD: "Subir"
|
||||||
|
STR_BOOK_S_STYLE: "Estilo del libro"
|
||||||
|
STR_EMBEDDED_STYLE: "Estilo integrado"
|
||||||
|
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||||
317
lib/I18n/translations/swedish.yaml
Normal file
317
lib/I18n/translations/swedish.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_language_name: "Svenska"
|
||||||
|
_language_code: "SWEDISH"
|
||||||
|
_order: "7"
|
||||||
|
|
||||||
|
STR_CROSSPOINT: "Crosspoint"
|
||||||
|
STR_BOOTING: "STARTAR"
|
||||||
|
STR_SLEEPING: "VILA"
|
||||||
|
STR_ENTERING_SLEEP: "Går i vila…"
|
||||||
|
STR_BROWSE_FILES: "Bläddra filer…"
|
||||||
|
STR_FILE_TRANSFER: "Filöverföring"
|
||||||
|
STR_SETTINGS_TITLE: "Inställningar"
|
||||||
|
STR_CALIBRE_LIBRARY: "Calibrebibliotek"
|
||||||
|
STR_CONTINUE_READING: "Fortsätt läsa"
|
||||||
|
STR_NO_OPEN_BOOK: "Ingen öppen bok"
|
||||||
|
STR_START_READING: "Börja läsa nedan"
|
||||||
|
STR_BOOKS: "Böcker"
|
||||||
|
STR_NO_BOOKS_FOUND: "Inga böcker hittade"
|
||||||
|
STR_SELECT_CHAPTER: "Välj kapitel"
|
||||||
|
STR_NO_CHAPTERS: "Inga kapitel"
|
||||||
|
STR_END_OF_BOOK: "Slutet på boken"
|
||||||
|
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
||||||
|
STR_INDEXING: "Indexerar…"
|
||||||
|
STR_MEMORY_ERROR: "Minnesfel"
|
||||||
|
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
||||||
|
STR_EMPTY_FILE: "Tom fil"
|
||||||
|
STR_OUT_OF_BOUNDS: "Utanför gränserna"
|
||||||
|
STR_LOADING: "Laddar…"
|
||||||
|
STR_LOAD_XTC_FAILED: "Misslyckades ladda XTC"
|
||||||
|
STR_LOAD_TXT_FAILED: "Misslyckades ladda TCT"
|
||||||
|
STR_LOAD_EPUB_FAILED: "Misslyckades ladda EPUB"
|
||||||
|
STR_SD_CARD_ERROR: "SD-kortfel"
|
||||||
|
STR_WIFI_NETWORKS: "Trådlösa nätverk"
|
||||||
|
STR_NO_NETWORKS: "Inga nätverk funna"
|
||||||
|
STR_NETWORKS_FOUND: "%zu nätverk funna"
|
||||||
|
STR_SCANNING: "Scannar…"
|
||||||
|
STR_CONNECTING: "Ansluter…"
|
||||||
|
STR_CONNECTED: "Ansluten!"
|
||||||
|
STR_CONNECTION_FAILED: "Anslutning misslyckades"
|
||||||
|
STR_CONNECTION_TIMEOUT: "Anslutnings timeout"
|
||||||
|
STR_FORGET_NETWORK: "Glöm nätverk?"
|
||||||
|
STR_SAVE_PASSWORD: "Spara lösenord till nästa gång?"
|
||||||
|
STR_REMOVE_PASSWORD: "Radera sparat lösenord?"
|
||||||
|
STR_PRESS_OK_SCAN: "Tryck OK för att skanna igen"
|
||||||
|
STR_PRESS_ANY_CONTINUE: "Tryck valfri knapp för att fortsätta"
|
||||||
|
STR_SELECT_HINT: "VÄNSTER/HÖGER: Välj OK: Bekräfta"
|
||||||
|
STR_HOW_CONNECT: "Hur vill du ansluta?"
|
||||||
|
STR_JOIN_NETWORK: "Anslut till ett nätverk"
|
||||||
|
STR_CREATE_HOTSPOT: "Skapa surfzon"
|
||||||
|
STR_JOIN_DESC: "Anslut till ett befintligt trådlöst nätverk"
|
||||||
|
STR_HOTSPOT_DESC: "Skapa ett trådlöst nätverk andra kan ansluta till"
|
||||||
|
STR_STARTING_HOTSPOT: "Startar surfzon…"
|
||||||
|
STR_HOTSPOT_MODE: "Surfzonsläge"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Anslut din enhet till detta trådlösa nätverk"
|
||||||
|
STR_OPEN_URL_HINT: "Öppna denna adress i din browser"
|
||||||
|
STR_OR_HTTP_PREFIX: "eller http://"
|
||||||
|
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
|
||||||
|
STR_CALIBRE_WIRELESS: "Calibre Trådlöst"
|
||||||
|
STR_CALIBRE_WEB_URL: "Calibre webbadress"
|
||||||
|
STR_CONNECT_WIRELESS: "Anslut som trådlös enhet"
|
||||||
|
STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
|
||||||
|
STR_MAC_ADDRESS: "MAC-adress:"
|
||||||
|
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Skriv in WiFi-lösenord"
|
||||||
|
STR_ENTER_TEXT: "Skriv text"
|
||||||
|
STR_TO_PREFIX: "till"
|
||||||
|
STR_CALIBRE_DISCOVERING: "Söker Calibre…"
|
||||||
|
STR_CALIBRE_CONNECTING_TO: "Ansluter till"
|
||||||
|
STR_CALIBRE_CONNECTED_TO: "Ansluten till"
|
||||||
|
STR_CALIBRE_WAITING_COMMANDS: "Väntar på kommandon…"
|
||||||
|
STR_CONNECTION_FAILED_RETRYING: "(Anslutning misslyckades. Försöker igen)"
|
||||||
|
STR_CALIBRE_DISCONNECTED: "Calibre nedkopplat"
|
||||||
|
STR_CALIBRE_WAITING_TRANSFER: "Väntar på överföring…"
|
||||||
|
STR_CALIBRE_TRANSFER_HINT: "Om överföring misslyckas: Aktivera\\n'Ignorera fritt utrymme' i Calibre's\\nSmartDevice plugin settings."
|
||||||
|
STR_CALIBRE_RECEIVING: "Tar emot:"
|
||||||
|
STR_CALIBRE_RECEIVED: "Mottaget:"
|
||||||
|
STR_CALIBRE_WAITING_MORE: "Väntar på mer.."
|
||||||
|
STR_CALIBRE_FAILED_CREATE_FILE: "Misslyckades att skapa fil"
|
||||||
|
STR_CALIBRE_PASSWORD_REQUIRED: "Lösenord krävs"
|
||||||
|
STR_CALIBRE_TRANSFER_INTERRUPTED: "Överföring avbröts"
|
||||||
|
STR_CALIBRE_INSTRUCTION_1: "1) Installera CrossPoint Reader plugin"
|
||||||
|
STR_CALIBRE_INSTRUCTION_2: "2) Anslut till samma trådlösa nätverk"
|
||||||
|
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: ”Skicka till enhet”"
|
||||||
|
STR_CALIBRE_INSTRUCTION_4: "”Håll denna skärm öppen under sändning”"
|
||||||
|
STR_CAT_DISPLAY: "Skärm"
|
||||||
|
STR_CAT_READER: "Läsare"
|
||||||
|
STR_CAT_CONTROLS: "Kontroller"
|
||||||
|
STR_CAT_SYSTEM: "System"
|
||||||
|
STR_SLEEP_SCREEN: "Viloskärm"
|
||||||
|
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
||||||
|
STR_STATUS_BAR: "Statusrad"
|
||||||
|
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
||||||
|
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
||||||
|
STR_TEXT_AA: "Textkantutjämning"
|
||||||
|
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||||
|
STR_ORIENTATION: "Läsrikting"
|
||||||
|
STR_FRONT_BTN_LAYOUT: "Frontknappslayout"
|
||||||
|
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
||||||
|
STR_LONG_PRESS_SKIP: "Lång-tryck Kapitelskippning"
|
||||||
|
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
||||||
|
STR_EXT_READER_FONT: "Extern Eboksläsartypsnitt"
|
||||||
|
STR_EXT_CHINESE_FONT: "Eboksläsartypsnitt"
|
||||||
|
STR_EXT_UI_FONT: "Användargränssnittets typsnitt"
|
||||||
|
STR_FONT_SIZE: "Användargränssnittets typsnittsstorlek"
|
||||||
|
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
|
||||||
|
STR_ASCII_LETTER_SPACING: "ASCII-bokstavsmellanrum"
|
||||||
|
STR_ASCII_DIGIT_SPACING: "ASCII-siffermellanrum"
|
||||||
|
STR_CJK_SPACING: "CJK-mellanrum"
|
||||||
|
STR_COLOR_MODE: "Färgläge"
|
||||||
|
STR_SCREEN_MARGIN: "Eboksläsarens skärmmarginal"
|
||||||
|
STR_PARA_ALIGNMENT: "Eboksläsarens paragraflinjeplacering"
|
||||||
|
STR_HYPHENATION: "Avstavning"
|
||||||
|
STR_TIME_TO_SLEEP: "Tid för att gå i vila"
|
||||||
|
STR_REFRESH_FREQ: "Uppdateringsfrekvens"
|
||||||
|
STR_CALIBRE_SETTINGS: "Calibreinställningar"
|
||||||
|
STR_KOREADER_SYNC: "KorReader-synkronisering"
|
||||||
|
STR_CHECK_UPDATES: "Kolla efter uppdateringar"
|
||||||
|
STR_LANGUAGE: "Språk"
|
||||||
|
STR_SELECT_WALLPAPER: "Välj bakgrundsbild"
|
||||||
|
STR_CLEAR_READING_CACHE: "Rensa Eboksläsarens cache"
|
||||||
|
STR_CALIBRE: "Calibre"
|
||||||
|
STR_USERNAME: "Användarnamn"
|
||||||
|
STR_PASSWORD: "Lösenord"
|
||||||
|
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
||||||
|
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
||||||
|
STR_AUTHENTICATE: "Autentisera "
|
||||||
|
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
||||||
|
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
||||||
|
STR_FILENAME: "Filnamn"
|
||||||
|
STR_BINARY: "Binär"
|
||||||
|
STR_SET_CREDENTIALS_FIRST: "Referenser"
|
||||||
|
STR_WIFI_CONN_FAILED: "Trådlös anslutning misslyckades"
|
||||||
|
STR_AUTHENTICATING: "Autentiserar…"
|
||||||
|
STR_AUTH_SUCCESS: "Lyckad autentisering!"
|
||||||
|
STR_KOREADER_AUTH: "KORreader autentisering"
|
||||||
|
STR_SYNC_READY: "KOReader synk är redo att användas"
|
||||||
|
STR_AUTH_FAILED: "Autentisering misslyckades"
|
||||||
|
STR_DONE: "Klar"
|
||||||
|
STR_CLEAR_CACHE_WARNING_1: "Detta rensar all cachad bokdata"
|
||||||
|
STR_CLEAR_CACHE_WARNING_2: "Alla läsframsteg kommer att försvinna!"
|
||||||
|
STR_CLEAR_CACHE_WARNING_3: "Böcker kommer att behöva omindexeras"
|
||||||
|
STR_CLEAR_CACHE_WARNING_4: "när de öppnas på nytt."
|
||||||
|
STR_CLEARING_CACHE: "Rensar cache…"
|
||||||
|
STR_CACHE_CLEARED: "Cache rensad!"
|
||||||
|
STR_ITEMS_REMOVED: "objekt raderade"
|
||||||
|
STR_FAILED_LOWER: "misslyckades "
|
||||||
|
STR_CLEAR_CACHE_FAILED: "Misslyckades att rensa cache"
|
||||||
|
STR_CHECK_SERIAL_OUTPUT: "Kolla seriell utgång för detaljer"
|
||||||
|
STR_DARK: "Mörk"
|
||||||
|
STR_LIGHT: "Ljus"
|
||||||
|
STR_CUSTOM: "Valfri"
|
||||||
|
STR_COVER: "Omslag"
|
||||||
|
STR_NONE_OPT: "Ingen öppen bok"
|
||||||
|
STR_FIT: "Passa"
|
||||||
|
STR_CROP: "Beskär"
|
||||||
|
STR_NO_PROGRESS: "Ingen framgång"
|
||||||
|
STR_FULL_OPT: "Full"
|
||||||
|
STR_NEVER: "Aldrig"
|
||||||
|
STR_IN_READER: "I Eboksläsare"
|
||||||
|
STR_ALWAYS: "Alltid"
|
||||||
|
STR_IGNORE: "Ignorera"
|
||||||
|
STR_SLEEP: "Vila"
|
||||||
|
STR_PAGE_TURN: "Sidvändning"
|
||||||
|
STR_PORTRAIT: "Porträtt"
|
||||||
|
STR_LANDSCAPE_CW: "Landskap medurs"
|
||||||
|
STR_INVERTED: "Inverterad"
|
||||||
|
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||||
|
STR_FRONT_LAYOUT_BCLR: "Bak, Bekr,Vän, Hög"
|
||||||
|
STR_FRONT_LAYOUT_LRBC: "Vän, Hög, Bak, Bekr"
|
||||||
|
STR_FRONT_LAYOUT_LBCR: "Vän, Bak, Bekr, Hög"
|
||||||
|
STR_PREV_NEXT: "Förra/Nästa"
|
||||||
|
STR_NEXT_PREV: "Nästa/Förra"
|
||||||
|
STR_BOOKERLY: "Bookerly"
|
||||||
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
|
STR_OPEN_DYSLEXIC: "Öppen dyslektisk"
|
||||||
|
STR_SMALL: "Liten"
|
||||||
|
STR_MEDIUM: "Medium"
|
||||||
|
STR_LARGE: "Stor"
|
||||||
|
STR_X_LARGE: "Extra stor"
|
||||||
|
STR_TIGHT: "Smal"
|
||||||
|
STR_NORMAL: "Normal"
|
||||||
|
STR_WIDE: "Bred"
|
||||||
|
STR_JUSTIFY: "Rättfärdiga"
|
||||||
|
STR_ALIGN_LEFT: "Vänster"
|
||||||
|
STR_CENTER: "Mitten"
|
||||||
|
STR_ALIGN_RIGHT: "Höger"
|
||||||
|
STR_MIN_1: "1 min"
|
||||||
|
STR_MIN_5: "5 min"
|
||||||
|
STR_MIN_10: "10 min"
|
||||||
|
STR_MIN_15: "15 min"
|
||||||
|
STR_MIN_30: "30 min"
|
||||||
|
STR_PAGES_1: "1 sida"
|
||||||
|
STR_PAGES_5: "5 sidor"
|
||||||
|
STR_PAGES_10: "10 sidor"
|
||||||
|
STR_PAGES_15: "15 sidor"
|
||||||
|
STR_PAGES_30: "30 sidor"
|
||||||
|
STR_UPDATE: "Uppdatera"
|
||||||
|
STR_CHECKING_UPDATE: "Söker uppdatering…"
|
||||||
|
STR_NEW_UPDATE: "Ny uppdatering tillgänglig!"
|
||||||
|
STR_CURRENT_VERSION: "Nuvarande version:"
|
||||||
|
STR_NEW_VERSION: "Ny version:"
|
||||||
|
STR_UPDATING: "Uppdaterar…"
|
||||||
|
STR_NO_UPDATE: "Ingen uppdatering tillgänglig"
|
||||||
|
STR_UPDATE_FAILED: "Uppdatering misslyckades"
|
||||||
|
STR_UPDATE_COMPLETE: "Uppdatering färdig"
|
||||||
|
STR_POWER_ON_HINT: "Tryck och håll strömknappen för att sätta på igen"
|
||||||
|
STR_EXTERNAL_FONT: "Externt typsnitt"
|
||||||
|
STR_BUILTIN_DISABLED: "Inbyggd (Avstängd)"
|
||||||
|
STR_NO_ENTRIES: "Inga poster funna"
|
||||||
|
STR_DOWNLOADING: "Laddar ner…"
|
||||||
|
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
|
||||||
|
STR_ERROR_MSG: "Fel:"
|
||||||
|
STR_UNNAMED: "Ej namngiven"
|
||||||
|
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
|
||||||
|
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
|
||||||
|
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
|
||||||
|
STR_NETWORK_PREFIX: "Nätverk:"
|
||||||
|
STR_IP_ADDRESS_PREFIX: "IP-adress;"
|
||||||
|
STR_SCAN_QR_WIFI_HINT: "eller skanna QR-kod med din telefon för att ansluta till WiFi."
|
||||||
|
STR_ERROR_GENERAL_FAILURE: "Fel: Generellt fel"
|
||||||
|
STR_ERROR_NETWORK_NOT_FOUND: "Fel: Nätverk hittades inte"
|
||||||
|
STR_ERROR_CONNECTION_TIMEOUT: "Fel: Anslutningstimeout"
|
||||||
|
STR_SD_CARD: "SD-kort"
|
||||||
|
STR_BACK: "« Bak"
|
||||||
|
STR_EXIT: "« Avsluta"
|
||||||
|
STR_HOME: "« Hem"
|
||||||
|
STR_SAVE: "« Spara"
|
||||||
|
STR_SELECT: "Välj "
|
||||||
|
STR_TOGGLE: "Växla"
|
||||||
|
STR_CONFIRM: "Bekräfta"
|
||||||
|
STR_CANCEL: "Avbryt"
|
||||||
|
STR_CONNECT: "Anslut"
|
||||||
|
STR_OPEN: "Öppna"
|
||||||
|
STR_DOWNLOAD: "Ladda ner"
|
||||||
|
STR_RETRY: "Försök igen"
|
||||||
|
STR_YES: "Ja"
|
||||||
|
STR_NO: "Nej"
|
||||||
|
STR_STATE_ON: "PÅ"
|
||||||
|
STR_STATE_OFF: "AV"
|
||||||
|
STR_SET: "Inställd"
|
||||||
|
STR_NOT_SET: "Inte inställd"
|
||||||
|
STR_DIR_LEFT: "Vänster"
|
||||||
|
STR_DIR_RIGHT: "Höger"
|
||||||
|
STR_DIR_UP: "Upp"
|
||||||
|
STR_DIR_DOWN: "Ner"
|
||||||
|
STR_CAPS_ON: "VERSALER"
|
||||||
|
STR_CAPS_OFF: "versaler"
|
||||||
|
STR_OK_BUTTON: "Okej"
|
||||||
|
STR_ON_MARKER: "[PÅ]"
|
||||||
|
STR_SLEEP_COVER_FILTER: "Viloskärmens omslagsfilter"
|
||||||
|
STR_FILTER_CONTRAST: "Kontrast"
|
||||||
|
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Procent"
|
||||||
|
STR_STATUS_BAR_FULL_BOOK: "Full w/ Boklist"
|
||||||
|
STR_STATUS_BAR_BOOK_ONLY: "Boklist enbart"
|
||||||
|
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist"
|
||||||
|
STR_UI_THEME: "Användargränssnittstema"
|
||||||
|
STR_THEME_CLASSIC: "Klassisk"
|
||||||
|
STR_THEME_LYRA: "Lyra"
|
||||||
|
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
||||||
|
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
||||||
|
STR_OPDS_BROWSER: "OPDS-webbläsare"
|
||||||
|
STR_COVER_CUSTOM: "Omslag + Valfri"
|
||||||
|
STR_RECENTS: "Senaste"
|
||||||
|
STR_MENU_RECENT_BOOKS: "Senaste böckerna"
|
||||||
|
STR_NO_RECENT_BOOKS: "Inga senaste böcker"
|
||||||
|
STR_CALIBRE_DESC: "Använd Calibres trådlösa enhetsöverföring"
|
||||||
|
STR_FORGET_AND_REMOVE: "Glöm nätverk och ta bort sparat lösenord?"
|
||||||
|
STR_FORGET_BUTTON: "Glöm nätverk"
|
||||||
|
STR_CALIBRE_STARTING: "Starar Calibre…"
|
||||||
|
STR_CALIBRE_SETUP: "Inställning"
|
||||||
|
STR_CALIBRE_STATUS: "Status"
|
||||||
|
STR_CLEAR_BUTTON: "Rensa"
|
||||||
|
STR_DEFAULT_VALUE: "Standard"
|
||||||
|
STR_REMAP_PROMPT: "Tryck en frontknapp för var funktion"
|
||||||
|
STR_UNASSIGNED: "Otilldelad"
|
||||||
|
STR_ALREADY_ASSIGNED: "Redan tilldelad"
|
||||||
|
STR_REMAP_RESET_HINT: "Översta sidoknapp: Återställ standardlayout"
|
||||||
|
STR_REMAP_CANCEL_HINT: "Nedre sidoknapp: Avbryt tilldelning"
|
||||||
|
STR_HW_BACK_LABEL: "Bak (Första knapp)"
|
||||||
|
STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knapp)"
|
||||||
|
STR_HW_LEFT_LABEL: "Vänster (Tredje knapp)"
|
||||||
|
STR_HW_RIGHT_LABEL: "Höger (Fjärde knapp)"
|
||||||
|
STR_GO_TO_PERCENT: "Gå till %"
|
||||||
|
STR_GO_HOME_BUTTON: "Gå Hem"
|
||||||
|
STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
|
||||||
|
STR_DELETE_CACHE: "Radera bokcache"
|
||||||
|
STR_CHAPTER_PREFIX: "Kapitel:"
|
||||||
|
STR_PAGES_SEPARATOR: " sidor | "
|
||||||
|
STR_BOOK_PREFIX: "Bok:"
|
||||||
|
STR_KBD_SHIFT: "shift"
|
||||||
|
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||||
|
STR_KBD_LOCK: "LOCK"
|
||||||
|
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
|
||||||
|
STR_PERCENT_STEP_HINT: "Vänster/Höger: 1% Upp/Ner 10%"
|
||||||
|
STR_SYNCING_TIME: "Synkroniserar tid…"
|
||||||
|
STR_CALC_HASH: "Beräknar dokumenthash"
|
||||||
|
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
|
||||||
|
STR_FETCH_PROGRESS: "Hämtar fjärrframsteg"
|
||||||
|
STR_UPLOAD_PROGRESS: "Laddar upp framsteg"
|
||||||
|
STR_NO_CREDENTIALS_MSG: "Inga uppgifter inställda"
|
||||||
|
STR_KOREADER_SETUP_HINT: "Ställ in KOReaderkonto i Inställningar"
|
||||||
|
STR_PROGRESS_FOUND: "Framsteg funna!"
|
||||||
|
STR_REMOTE_LABEL: "Fjärr:"
|
||||||
|
STR_LOCAL_LABEL: "Lokalt:"
|
||||||
|
STR_PAGE_OVERALL_FORMAT: "Sida %d, %.2f%% totalt"
|
||||||
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Sida %d/%d, %.2f%% totalt"
|
||||||
|
STR_DEVICE_FROM_FORMAT: " Från: %s"
|
||||||
|
STR_APPLY_REMOTE: "Använd fjärrframsteg"
|
||||||
|
STR_UPLOAD_LOCAL: "Ladda upp lokala framsteg"
|
||||||
|
STR_NO_REMOTE_MSG: "Inga fjärrframsteg funna"
|
||||||
|
STR_UPLOAD_PROMPT: "Ladda upp nuvarande position?"
|
||||||
|
STR_UPLOAD_SUCCESS: "Framsteg uppladdade!"
|
||||||
|
STR_SYNC_FAILED_MSG: "Synkronisering misslyckades"
|
||||||
|
STR_SECTION_PREFIX: "Sektion"
|
||||||
|
STR_UPLOAD: "Uppladdning"
|
||||||
|
STR_BOOK_S_STYLE: "Bokstil"
|
||||||
|
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||||
|
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "JpegToBmpConverter.h"
|
#include "JpegToBmpConverter.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SdFat.h>
|
#include <Logging.h>
|
||||||
#include <picojpeg.h>
|
#include <picojpeg.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -201,8 +201,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
|
|||||||
// Internal implementation with configurable target size and bit depth
|
// Internal implementation with configurable target size and bit depth
|
||||||
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||||
bool oneBit, bool crop) {
|
bool oneBit, bool crop) {
|
||||||
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit",
|
LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||||
targetWidth, targetHeight);
|
|
||||||
|
|
||||||
// Setup context for picojpeg callback
|
// Setup context for picojpeg callback
|
||||||
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
||||||
@@ -211,12 +210,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
pjpeg_image_info_t imageInfo;
|
pjpeg_image_info_t imageInfo;
|
||||||
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
Serial.printf("[%lu] [JPG] JPEG decode init failed with error code: %d\n", millis(), status);
|
LOG_ERR("JPG", "JPEG decode init failed with error code: %d", status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d\n", millis(), imageInfo.m_width,
|
LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||||
imageInfo.m_height, imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
||||||
|
|
||||||
// Safety limits to prevent memory issues on ESP32
|
// Safety limits to prevent memory issues on ESP32
|
||||||
constexpr int MAX_IMAGE_WIDTH = 2048;
|
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||||
@@ -224,8 +223,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
constexpr int MAX_MCU_ROW_BYTES = 65536;
|
constexpr int MAX_MCU_ROW_BYTES = 65536;
|
||||||
|
|
||||||
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
|
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
|
||||||
Serial.printf("[%lu] [JPG] Image too large (%dx%d), max supported: %dx%d\n", millis(), imageInfo.m_width,
|
LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||||
imageInfo.m_height, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +261,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
|
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
|
||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
|
LOG_DBG("JPG", "Pre-scaling %dx%d -> %dx%d (fit to %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth,
|
||||||
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight);
|
outHeight, targetWidth, targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
@@ -282,7 +281,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
// Allocate row buffer
|
// Allocate row buffer
|
||||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
if (!rowBuffer) {
|
if (!rowBuffer) {
|
||||||
Serial.printf("[%lu] [JPG] Failed to allocate row buffer\n", millis());
|
LOG_ERR("JPG", "Failed to allocate row buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,15 +292,14 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
|
|
||||||
// Validate MCU row buffer size before allocation
|
// Validate MCU row buffer size before allocation
|
||||||
if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
|
if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
|
||||||
Serial.printf("[%lu] [JPG] MCU row buffer too large (%d bytes), max: %d\n", millis(), mcuRowPixels,
|
LOG_DBG("JPG", "MCU row buffer too large (%d bytes), max: %d", mcuRowPixels, MAX_MCU_ROW_BYTES);
|
||||||
MAX_MCU_ROW_BYTES);
|
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
|
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
|
||||||
if (!mcuRowBuffer) {
|
if (!mcuRowBuffer) {
|
||||||
Serial.printf("[%lu] [JPG] Failed to allocate MCU row buffer (%d bytes)\n", millis(), mcuRowPixels);
|
LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels);
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -349,10 +347,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
const unsigned char mcuStatus = pjpeg_decode_mcu();
|
const unsigned char mcuStatus = pjpeg_decode_mcu();
|
||||||
if (mcuStatus != 0) {
|
if (mcuStatus != 0) {
|
||||||
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
|
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
|
||||||
Serial.printf("[%lu] [JPG] Unexpected end of blocks at MCU (%d, %d)\n", millis(), mcuX, mcuY);
|
LOG_ERR("JPG", "Unexpected end of blocks at MCU (%d, %d)", mcuX, mcuY);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [JPG] JPEG decode MCU failed at (%d, %d) with error code: %d\n", millis(), mcuX, mcuY,
|
LOG_ERR("JPG", "JPEG decode MCU failed at (%d, %d) with error code: %d", mcuX, mcuY, mcuStatus);
|
||||||
mcuStatus);
|
|
||||||
}
|
}
|
||||||
free(mcuRowBuffer);
|
free(mcuRowBuffer);
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
@@ -549,7 +546,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
free(mcuRowBuffer);
|
free(mcuRowBuffer);
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis());
|
LOG_DBG("JPG", "Successfully converted JPEG to BMP");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,5 +564,5 @@ bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bm
|
|||||||
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
|
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
|
||||||
bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth,
|
bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth,
|
||||||
int targetMaxHeight) {
|
int targetMaxHeight) {
|
||||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
|
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
@@ -32,10 +32,10 @@ void KOReaderCredentialStore::obfuscate(std::string& data) const {
|
|||||||
|
|
||||||
bool KOReaderCredentialStore::saveToFile() const {
|
bool KOReaderCredentialStore::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
if (!Storage.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ bool KOReaderCredentialStore::saveToFile() const {
|
|||||||
|
|
||||||
// Write username (plaintext - not particularly sensitive)
|
// Write username (plaintext - not particularly sensitive)
|
||||||
serialization::writeString(file, username);
|
serialization::writeString(file, username);
|
||||||
Serial.printf("[%lu] [KRS] Saving username: %s\n", millis(), username.c_str());
|
LOG_DBG("KRS", "Saving username: %s", username.c_str());
|
||||||
|
|
||||||
// Write password (obfuscated)
|
// Write password (obfuscated)
|
||||||
std::string obfuscatedPwd = password;
|
std::string obfuscatedPwd = password;
|
||||||
@@ -58,14 +58,14 @@ bool KOReaderCredentialStore::saveToFile() const {
|
|||||||
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
|
LOG_DBG("KRS", "Saved KOReader credentials to file");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KOReaderCredentialStore::loadFromFile() {
|
bool KOReaderCredentialStore::loadFromFile() {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) {
|
if (!Storage.openFileForRead("KRS", KOREADER_FILE, file)) {
|
||||||
Serial.printf("[%lu] [KRS] No credentials file found\n", millis());
|
LOG_DBG("KRS", "No credentials file found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ bool KOReaderCredentialStore::loadFromFile() {
|
|||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != KOREADER_FILE_VERSION) {
|
if (version != KOREADER_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [KRS] Unknown file version: %u\n", millis(), version);
|
LOG_DBG("KRS", "Unknown file version: %u", version);
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -110,14 +110,14 @@ bool KOReaderCredentialStore::loadFromFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
|
LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", username.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
||||||
username = user;
|
username = user;
|
||||||
password = pass;
|
password = pass;
|
||||||
Serial.printf("[%lu] [KRS] Set credentials for user: %s\n", millis(), user.c_str());
|
LOG_DBG("KRS", "Set credentials for user: %s", user.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string KOReaderCredentialStore::getMd5Password() const {
|
std::string KOReaderCredentialStore::getMd5Password() const {
|
||||||
@@ -140,12 +140,12 @@ void KOReaderCredentialStore::clearCredentials() {
|
|||||||
username.clear();
|
username.clear();
|
||||||
password.clear();
|
password.clear();
|
||||||
saveToFile();
|
saveToFile();
|
||||||
Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis());
|
LOG_DBG("KRS", "Cleared KOReader credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
||||||
serverUrl = url;
|
serverUrl = url;
|
||||||
Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str());
|
LOG_DBG("KRS", "Set server URL: %s", url.empty() ? "(default)" : url.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string KOReaderCredentialStore::getBaseUrl() const {
|
std::string KOReaderCredentialStore::getBaseUrl() const {
|
||||||
@@ -163,6 +163,5 @@ std::string KOReaderCredentialStore::getBaseUrl() const {
|
|||||||
|
|
||||||
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
||||||
matchMethod = method;
|
matchMethod = method;
|
||||||
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(),
|
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||||
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "KOReaderDocumentId.h"
|
#include "KOReaderDocumentId.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Extract filename from path (everything after last '/')
|
// Extract filename from path (everything after last '/')
|
||||||
@@ -27,7 +27,7 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat
|
|||||||
md5.calculate();
|
md5.calculate();
|
||||||
|
|
||||||
std::string result = md5.toString().c_str();
|
std::string result = md5.toString().c_str();
|
||||||
Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str());
|
LOG_DBG("KODoc", "Filename hash: %s (from '%s')", result.c_str(), filename.c_str());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +43,13 @@ size_t KOReaderDocumentId::getOffset(int i) {
|
|||||||
|
|
||||||
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("KODoc", filePath, file)) {
|
if (!Storage.openFileForRead("KODoc", filePath, file)) {
|
||||||
Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str());
|
LOG_DBG("KODoc", "Failed to open file: %s", filePath.c_str());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t fileSize = file.fileSize();
|
const size_t fileSize = file.fileSize();
|
||||||
Serial.printf("[%lu] [KODoc] Calculating hash for file: %s (size: %zu)\n", millis(), filePath.c_str(), fileSize);
|
LOG_DBG("KODoc", "Calculating hash for file: %s (size: %zu)", filePath.c_str(), fileSize);
|
||||||
|
|
||||||
// Initialize MD5 builder
|
// Initialize MD5 builder
|
||||||
MD5Builder md5;
|
MD5Builder md5;
|
||||||
@@ -70,7 +70,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|||||||
|
|
||||||
// Seek to offset
|
// Seek to offset
|
||||||
if (!file.seekSet(offset)) {
|
if (!file.seekSet(offset)) {
|
||||||
Serial.printf("[%lu] [KODoc] Failed to seek to offset %zu\n", millis(), offset);
|
LOG_DBG("KODoc", "Failed to seek to offset %zu", offset);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|||||||
md5.calculate();
|
md5.calculate();
|
||||||
std::string result = md5.toString().c_str();
|
std::string result = md5.toString().c_str();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KODoc] Hash calculated: %s (from %zu bytes)\n", millis(), result.c_str(), totalBytesRead);
|
LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <WiFiClientSecure.h>
|
#include <WiFiClientSecure.h>
|
||||||
|
|
||||||
@@ -19,6 +19,10 @@ void addAuthHeaders(HTTPClient& http) {
|
|||||||
http.addHeader("Accept", "application/vnd.koreader.v1+json");
|
http.addHeader("Accept", "application/vnd.koreader.v1+json");
|
||||||
http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str());
|
http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str());
|
||||||
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str());
|
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str());
|
||||||
|
|
||||||
|
// HTTP Basic Auth (RFC 7617) header. This is needed to support koreader sync server embedded in Calibre Web Automated
|
||||||
|
// (https://github.com/crocodilestick/Calibre-Web-Automated/blob/main/cps/progress_syncing/protocols/kosync.py)
|
||||||
|
http.setAuthorization(KOREADER_STORE.getUsername().c_str(), KOREADER_STORE.getPassword().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
|
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
|
||||||
@@ -26,12 +30,12 @@ bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0;
|
|||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
LOG_DBG("KOSync", "No credentials configured");
|
||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
||||||
Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str());
|
LOG_DBG("KOSync", "Authenticating: %s", url.c_str());
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||||
@@ -49,7 +53,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
|||||||
const int httpCode = http.GET();
|
const int httpCode = http.GET();
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Auth response: %d\n", millis(), httpCode);
|
LOG_DBG("KOSync", "Auth response: %d", httpCode);
|
||||||
|
|
||||||
if (httpCode == 200) {
|
if (httpCode == 200) {
|
||||||
return OK;
|
return OK;
|
||||||
@@ -64,12 +68,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
|||||||
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
|
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
|
||||||
KOReaderProgress& outProgress) {
|
KOReaderProgress& outProgress) {
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
LOG_DBG("KOSync", "No credentials configured");
|
||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
||||||
Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str());
|
LOG_DBG("KOSync", "Getting progress: %s", url.c_str());
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||||
@@ -95,7 +99,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
const DeserializationError error = deserializeJson(doc, responseBody);
|
const DeserializationError error = deserializeJson(doc, responseBody);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
Serial.printf("[%lu] [KOSync] JSON parse failed: %s\n", millis(), error.c_str());
|
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
||||||
return JSON_ERROR;
|
return JSON_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,14 +110,13 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
outProgress.deviceId = doc["device_id"].as<std::string>();
|
outProgress.deviceId = doc["device_id"].as<std::string>();
|
||||||
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Got progress: %.2f%% at %s\n", millis(), outProgress.percentage * 100,
|
LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
|
||||||
outProgress.progress.c_str());
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Get progress response: %d\n", millis(), httpCode);
|
LOG_DBG("KOSync", "Get progress response: %d", httpCode);
|
||||||
|
|
||||||
if (httpCode == 401) {
|
if (httpCode == 401) {
|
||||||
return AUTH_FAILED;
|
return AUTH_FAILED;
|
||||||
@@ -127,12 +130,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
|
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
LOG_DBG("KOSync", "No credentials configured");
|
||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
||||||
Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str());
|
LOG_DBG("KOSync", "Updating progress: %s", url.c_str());
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||||
@@ -159,12 +162,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
std::string body;
|
std::string body;
|
||||||
serializeJson(doc, body);
|
serializeJson(doc, body);
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Request body: %s\n", millis(), body.c_str());
|
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
||||||
|
|
||||||
const int httpCode = http.PUT(body.c_str());
|
const int httpCode = http.PUT(body.c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Update progress response: %d\n", millis(), httpCode);
|
LOG_DBG("KOSync", "Update progress response: %d", httpCode);
|
||||||
|
|
||||||
if (httpCode == 200 || httpCode == 202) {
|
if (httpCode == 200 || httpCode == 202) {
|
||||||
return OK;
|
return OK;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "ProgressMapper.h"
|
#include "ProgressMapper.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
|
|||||||
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
|
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
|
||||||
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
|
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
|
||||||
|
|
||||||
Serial.printf("[%lu] [ProgressMapper] CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s\n", millis(),
|
LOG_DBG("ProgressMapper", "CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s", chapterName.c_str(),
|
||||||
chapterName.c_str(), pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -76,8 +76,8 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ProgressMapper] KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d\n", millis(),
|
LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d", koPos.percentage * 100,
|
||||||
koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
47
lib/Logging/Logging.cpp
Normal file
47
lib/Logging/Logging.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "Logging.h"
|
||||||
|
|
||||||
|
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
|
||||||
|
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
|
||||||
|
// provide the format string for the message itself.
|
||||||
|
void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
||||||
|
if (!logSerial) {
|
||||||
|
return; // Serial not initialized, skip logging
|
||||||
|
}
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
char buf[256];
|
||||||
|
char* c = buf;
|
||||||
|
// add the timestamp
|
||||||
|
{
|
||||||
|
unsigned long ms = millis();
|
||||||
|
int len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||||
|
if (len < 0) {
|
||||||
|
return; // encoding error, skip logging
|
||||||
|
}
|
||||||
|
c += len;
|
||||||
|
}
|
||||||
|
// add the level
|
||||||
|
{
|
||||||
|
const char* p = level;
|
||||||
|
size_t remaining = sizeof(buf) - (c - buf);
|
||||||
|
while (*p && remaining > 1) {
|
||||||
|
*c++ = *p++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
if (remaining > 1) {
|
||||||
|
*c++ = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add the origin
|
||||||
|
{
|
||||||
|
int len = snprintf(c, sizeof(buf) - (c - buf), "[%s] ", origin);
|
||||||
|
if (len < 0) {
|
||||||
|
return; // encoding error, skip logging
|
||||||
|
}
|
||||||
|
c += len;
|
||||||
|
}
|
||||||
|
// add the user message
|
||||||
|
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
|
||||||
|
va_end(args);
|
||||||
|
logSerial.print(buf);
|
||||||
|
}
|
||||||
71
lib/Logging/Logging.h
Normal file
71
lib/Logging/Logging.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define ENABLE_SERIAL_LOG to enable logging
|
||||||
|
Can be set in platformio.ini build_flags or as a compile definition
|
||||||
|
|
||||||
|
Define LOG_LEVEL to control log verbosity:
|
||||||
|
0 = ERR only
|
||||||
|
1 = ERR + INF
|
||||||
|
2 = ERR + INF + DBG
|
||||||
|
If not defined, defaults to 0
|
||||||
|
|
||||||
|
If you have a legitimate need for raw Serial access (e.g., binary data,
|
||||||
|
special formatting), use the underlying logSerial object directly:
|
||||||
|
logSerial.printf("Special case: %d\n", value);
|
||||||
|
logSerial.write(binaryData, length);
|
||||||
|
|
||||||
|
The logSerial reference (defined below) points to the real Serial object and
|
||||||
|
won't trigger deprecation warnings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LOG_LEVEL
|
||||||
|
#define LOG_LEVEL 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static HWCDC& logSerial = Serial;
|
||||||
|
|
||||||
|
void logPrintf(const char* level, const char* origin, const char* format, ...);
|
||||||
|
|
||||||
|
#ifdef ENABLE_SERIAL_LOG
|
||||||
|
#if LOG_LEVEL >= 0
|
||||||
|
#define LOG_ERR(origin, format, ...) logPrintf("[ERR]", origin, format "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOG_ERR(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LOG_LEVEL >= 1
|
||||||
|
#define LOG_INF(origin, format, ...) logPrintf("[INF]", origin, format "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOG_INF(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LOG_LEVEL >= 2
|
||||||
|
#define LOG_DBG(origin, format, ...) logPrintf("[DBG]", origin, format "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOG_DBG(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define LOG_DBG(origin, format, ...)
|
||||||
|
#define LOG_ERR(origin, format, ...)
|
||||||
|
#define LOG_INF(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class MySerialImpl : public Print {
|
||||||
|
public:
|
||||||
|
void begin(unsigned long baud) { logSerial.begin(baud); }
|
||||||
|
|
||||||
|
// Support boolean conversion for compatibility with code like:
|
||||||
|
// if (Serial) or while (!Serial)
|
||||||
|
operator bool() const { return logSerial; }
|
||||||
|
|
||||||
|
__attribute__((deprecated("Use LOG_* macro instead"))) size_t printf(const char* format, ...);
|
||||||
|
size_t write(uint8_t b) override;
|
||||||
|
size_t write(const uint8_t* buffer, size_t size) override;
|
||||||
|
void flush() override;
|
||||||
|
static MySerialImpl instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define Serial MySerialImpl::instance
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "OpdsParser.h"
|
#include "OpdsParser.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ OpdsParser::OpdsParser() {
|
|||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
|||||||
void* const buf = XML_GetBuffer(parser, chunkSize);
|
void* const buf = XML_GetBuffer(parser, chunkSize);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis());
|
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
return length;
|
return length;
|
||||||
@@ -53,8 +53,8 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
|||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
return length;
|
return length;
|
||||||
|
|||||||
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
#include "PngToBmpConverter.h"
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <miniz.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "BitmapHelpers.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IMAGE PROCESSING OPTIONS - Same as JpegToBmpConverter for consistency
|
||||||
|
// ============================================================================
|
||||||
|
constexpr bool USE_8BIT_OUTPUT = false;
|
||||||
|
constexpr bool USE_ATKINSON = true;
|
||||||
|
constexpr bool USE_FLOYD_STEINBERG = false;
|
||||||
|
constexpr bool USE_PRESCALE = true;
|
||||||
|
constexpr int TARGET_MAX_WIDTH = 480;
|
||||||
|
constexpr int TARGET_MAX_HEIGHT = 800;
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// PNG constants
|
||||||
|
static constexpr uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10};
|
||||||
|
|
||||||
|
// PNG color types
|
||||||
|
enum PngColorType : uint8_t {
|
||||||
|
PNG_COLOR_GRAYSCALE = 0,
|
||||||
|
PNG_COLOR_RGB = 2,
|
||||||
|
PNG_COLOR_PALETTE = 3,
|
||||||
|
PNG_COLOR_GRAYSCALE_ALPHA = 4,
|
||||||
|
PNG_COLOR_RGBA = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// PNG filter types
|
||||||
|
enum PngFilter : uint8_t {
|
||||||
|
PNG_FILTER_NONE = 0,
|
||||||
|
PNG_FILTER_SUB = 1,
|
||||||
|
PNG_FILTER_UP = 2,
|
||||||
|
PNG_FILTER_AVERAGE = 3,
|
||||||
|
PNG_FILTER_PAETH = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read a big-endian 32-bit value from file
|
||||||
|
static bool readBE32(FsFile& file, uint32_t& value) {
|
||||||
|
uint8_t buf[4];
|
||||||
|
if (file.read(buf, 4) != 4) return false;
|
||||||
|
value = (static_cast<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
|
||||||
|
(static_cast<uint32_t>(buf[2]) << 8) | buf[3];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BMP writing helpers (same as JpegToBmpConverter)
|
||||||
|
inline void write16(Print& out, const uint16_t value) {
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void write32(Print& out, const uint32_t value) {
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
out.write((value >> 16) & 0xFF);
|
||||||
|
out.write((value >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void write32Signed(Print& out, const int32_t value) {
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
out.write((value >> 16) & 0xFF);
|
||||||
|
out.write((value >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
||||||
|
const int bytesPerRow = (width + 3) / 4 * 4;
|
||||||
|
const int imageSize = bytesPerRow * height;
|
||||||
|
const uint32_t paletteSize = 256 * 4;
|
||||||
|
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
|
||||||
|
|
||||||
|
bmpOut.write('B');
|
||||||
|
bmpOut.write('M');
|
||||||
|
write32(bmpOut, fileSize);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, 14 + 40 + paletteSize);
|
||||||
|
|
||||||
|
write32(bmpOut, 40);
|
||||||
|
write32Signed(bmpOut, width);
|
||||||
|
write32Signed(bmpOut, -height);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write16(bmpOut, 8);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, imageSize);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 256);
|
||||||
|
write32(bmpOut, 256);
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
bmpOut.write(static_cast<uint8_t>(i));
|
||||||
|
bmpOut.write(static_cast<uint8_t>(i));
|
||||||
|
bmpOut.write(static_cast<uint8_t>(i));
|
||||||
|
bmpOut.write(static_cast<uint8_t>(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
|
||||||
|
const int bytesPerRow = (width + 31) / 32 * 4;
|
||||||
|
const int imageSize = bytesPerRow * height;
|
||||||
|
const uint32_t fileSize = 62 + imageSize;
|
||||||
|
|
||||||
|
bmpOut.write('B');
|
||||||
|
bmpOut.write('M');
|
||||||
|
write32(bmpOut, fileSize);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, 62);
|
||||||
|
|
||||||
|
write32(bmpOut, 40);
|
||||||
|
write32Signed(bmpOut, width);
|
||||||
|
write32Signed(bmpOut, -height);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, imageSize);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2);
|
||||||
|
write32(bmpOut, 2);
|
||||||
|
|
||||||
|
uint8_t palette[8] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
|
||||||
|
for (const uint8_t i : palette) {
|
||||||
|
bmpOut.write(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
|
||||||
|
const int bytesPerRow = (width * 2 + 31) / 32 * 4;
|
||||||
|
const int imageSize = bytesPerRow * height;
|
||||||
|
const uint32_t fileSize = 70 + imageSize;
|
||||||
|
|
||||||
|
bmpOut.write('B');
|
||||||
|
bmpOut.write('M');
|
||||||
|
write32(bmpOut, fileSize);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, 70);
|
||||||
|
|
||||||
|
write32(bmpOut, 40);
|
||||||
|
write32Signed(bmpOut, width);
|
||||||
|
write32Signed(bmpOut, -height);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write16(bmpOut, 2);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, imageSize);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 4);
|
||||||
|
write32(bmpOut, 4);
|
||||||
|
|
||||||
|
uint8_t palette[16] = {0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x00,
|
||||||
|
0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
|
||||||
|
for (const uint8_t i : palette) {
|
||||||
|
bmpOut.write(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paeth predictor function per PNG spec
|
||||||
|
static inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) {
|
||||||
|
int p = static_cast<int>(a) + b - c;
|
||||||
|
int pa = p > a ? p - a : a - p;
|
||||||
|
int pb = p > b ? p - b : b - p;
|
||||||
|
int pc = p > c ? p - c : c - p;
|
||||||
|
if (pa <= pb && pa <= pc) return a;
|
||||||
|
if (pb <= pc) return b;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context for streaming PNG decompression
|
||||||
|
struct PngDecodeContext {
|
||||||
|
FsFile& file;
|
||||||
|
|
||||||
|
// PNG image properties
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint8_t bitDepth;
|
||||||
|
uint8_t colorType;
|
||||||
|
uint8_t bytesPerPixel; // after expanding sub-byte depths
|
||||||
|
uint32_t rawRowBytes; // bytes per raw row (without filter byte)
|
||||||
|
|
||||||
|
// Scanline buffers
|
||||||
|
uint8_t* currentRow; // current defiltered scanline
|
||||||
|
uint8_t* previousRow; // previous defiltered scanline
|
||||||
|
|
||||||
|
// zlib decompression state
|
||||||
|
mz_stream zstream;
|
||||||
|
bool zstreamInitialized;
|
||||||
|
|
||||||
|
// Chunk reading state
|
||||||
|
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
|
||||||
|
bool idatFinished; // no more IDAT chunks
|
||||||
|
|
||||||
|
// File read buffer for feeding zlib
|
||||||
|
uint8_t readBuf[2048];
|
||||||
|
|
||||||
|
// Palette for indexed color (type 3)
|
||||||
|
uint8_t palette[256 * 3];
|
||||||
|
int paletteSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the next IDAT chunk header, skipping non-IDAT chunks
|
||||||
|
// Returns true if an IDAT chunk was found
|
||||||
|
static bool findNextIdatChunk(PngDecodeContext& ctx) {
|
||||||
|
while (true) {
|
||||||
|
uint32_t chunkLen;
|
||||||
|
if (!readBE32(ctx.file, chunkLen)) return false;
|
||||||
|
|
||||||
|
uint8_t chunkType[4];
|
||||||
|
if (ctx.file.read(chunkType, 4) != 4) return false;
|
||||||
|
|
||||||
|
if (memcmp(chunkType, "IDAT", 4) == 0) {
|
||||||
|
ctx.chunkBytesRemaining = chunkLen;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip this chunk's data + 4-byte CRC
|
||||||
|
// Use seek to skip efficiently
|
||||||
|
if (!ctx.file.seekCur(chunkLen + 4)) return false;
|
||||||
|
|
||||||
|
// If we hit IEND, there are no more chunks
|
||||||
|
if (memcmp(chunkType, "IEND", 4) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed compressed data to zlib from IDAT chunks
|
||||||
|
// Returns number of bytes made available in zstream, or -1 on error
|
||||||
|
static int feedZlibInput(PngDecodeContext& ctx) {
|
||||||
|
if (ctx.idatFinished) return 0;
|
||||||
|
|
||||||
|
// If current IDAT chunk is exhausted, skip its CRC and find next
|
||||||
|
while (ctx.chunkBytesRemaining == 0) {
|
||||||
|
// Skip 4-byte CRC of previous IDAT
|
||||||
|
if (!ctx.file.seekCur(4)) return -1;
|
||||||
|
|
||||||
|
if (!findNextIdatChunk(ctx)) {
|
||||||
|
ctx.idatFinished = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from current IDAT chunk
|
||||||
|
size_t toRead = sizeof(ctx.readBuf);
|
||||||
|
if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining;
|
||||||
|
|
||||||
|
int bytesRead = ctx.file.read(ctx.readBuf, toRead);
|
||||||
|
if (bytesRead <= 0) return -1;
|
||||||
|
|
||||||
|
ctx.chunkBytesRemaining -= bytesRead;
|
||||||
|
ctx.zstream.next_in = ctx.readBuf;
|
||||||
|
ctx.zstream.avail_in = bytesRead;
|
||||||
|
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompress exactly 'needed' bytes into 'dest'
|
||||||
|
static bool decompressBytes(PngDecodeContext& ctx, uint8_t* dest, size_t needed) {
|
||||||
|
ctx.zstream.next_out = dest;
|
||||||
|
ctx.zstream.avail_out = needed;
|
||||||
|
|
||||||
|
while (ctx.zstream.avail_out > 0) {
|
||||||
|
if (ctx.zstream.avail_in == 0) {
|
||||||
|
int fed = feedZlibInput(ctx);
|
||||||
|
if (fed < 0) return false;
|
||||||
|
if (fed == 0) {
|
||||||
|
// Try one more inflate to flush
|
||||||
|
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
|
||||||
|
if (ctx.zstream.avail_out == 0) break;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
|
||||||
|
if (ret != MZ_OK && ret != MZ_STREAM_END && ret != MZ_BUF_ERROR) {
|
||||||
|
LOG_ERR("PNG", "zlib inflate error: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ret == MZ_STREAM_END) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.zstream.avail_out == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one scanline: decompress filter byte + raw bytes, then unfilter
|
||||||
|
static bool decodeScanline(PngDecodeContext& ctx) {
|
||||||
|
// Decompress filter byte
|
||||||
|
uint8_t filterType;
|
||||||
|
if (!decompressBytes(ctx, &filterType, 1)) return false;
|
||||||
|
|
||||||
|
// Decompress raw row data into currentRow
|
||||||
|
if (!decompressBytes(ctx, ctx.currentRow, ctx.rawRowBytes)) return false;
|
||||||
|
|
||||||
|
// Apply reverse filter
|
||||||
|
const int bpp = ctx.bytesPerPixel;
|
||||||
|
|
||||||
|
switch (filterType) {
|
||||||
|
case PNG_FILTER_NONE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_SUB:
|
||||||
|
for (uint32_t i = bpp; i < ctx.rawRowBytes; i++) {
|
||||||
|
ctx.currentRow[i] += ctx.currentRow[i - bpp];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_UP:
|
||||||
|
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||||
|
ctx.currentRow[i] += ctx.previousRow[i];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_AVERAGE:
|
||||||
|
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||||
|
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||||
|
uint8_t b = ctx.previousRow[i];
|
||||||
|
ctx.currentRow[i] += (a + b) / 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_PAETH:
|
||||||
|
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||||
|
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||||
|
uint8_t b = ctx.previousRow[i];
|
||||||
|
uint8_t c = (i >= static_cast<uint32_t>(bpp)) ? ctx.previousRow[i - bpp] : 0;
|
||||||
|
ctx.currentRow[i] += paethPredictor(a, b, c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG_ERR("PNG", "Unknown filter type: %d", filterType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-convert an entire scanline to grayscale.
|
||||||
|
// Branches once on colorType/bitDepth, then runs a tight loop for the whole row.
|
||||||
|
static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) {
|
||||||
|
const uint8_t* src = ctx.currentRow;
|
||||||
|
const uint32_t w = ctx.width;
|
||||||
|
|
||||||
|
switch (ctx.colorType) {
|
||||||
|
case PNG_COLOR_GRAYSCALE:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
memcpy(grayRow, src, w);
|
||||||
|
} else if (ctx.bitDepth == 16) {
|
||||||
|
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
|
||||||
|
} else {
|
||||||
|
const int ppb = 8 / ctx.bitDepth;
|
||||||
|
const uint8_t mask = (1 << ctx.bitDepth) - 1;
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
|
||||||
|
grayRow[x] = (src[x / ppb] >> shift & mask) * 255 / mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_COLOR_RGB:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
// Fast path: most common EPUB cover format
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
const uint8_t* p = src + x * 3;
|
||||||
|
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
grayRow[x] = (src[x * 6] * 25 + src[x * 6 + 2] * 50 + src[x * 6 + 4] * 25) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_COLOR_PALETTE: {
|
||||||
|
const int ppb = 8 / ctx.bitDepth;
|
||||||
|
const uint8_t mask = (1 << ctx.bitDepth) - 1;
|
||||||
|
const uint8_t* pal = ctx.palette;
|
||||||
|
const int palSize = ctx.paletteSize;
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
|
||||||
|
uint8_t idx = (src[x / ppb] >> shift) & mask;
|
||||||
|
if (idx >= palSize) idx = 0;
|
||||||
|
grayRow[x] = (pal[idx * 3] * 25 + pal[idx * 3 + 1] * 50 + pal[idx * 3 + 2] * 25) / 100;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PNG_COLOR_GRAYSCALE_ALPHA:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
|
||||||
|
} else {
|
||||||
|
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 4];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_COLOR_RGBA:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
const uint8_t* p = src + x * 4;
|
||||||
|
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
grayRow[x] = (src[x * 8] * 25 + src[x * 8 + 2] * 50 + src[x * 8 + 4] * 25) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
memset(grayRow, 128, w);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||||
|
bool oneBit, bool crop) {
|
||||||
|
LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||||
|
|
||||||
|
// Verify PNG signature
|
||||||
|
uint8_t sig[8];
|
||||||
|
if (pngFile.read(sig, 8) != 8 || memcmp(sig, PNG_SIGNATURE, 8) != 0) {
|
||||||
|
LOG_ERR("PNG", "Invalid PNG signature");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read IHDR chunk
|
||||||
|
uint32_t ihdrLen;
|
||||||
|
if (!readBE32(pngFile, ihdrLen)) return false;
|
||||||
|
|
||||||
|
uint8_t ihdrType[4];
|
||||||
|
if (pngFile.read(ihdrType, 4) != 4 || memcmp(ihdrType, "IHDR", 4) != 0) {
|
||||||
|
LOG_ERR("PNG", "Missing IHDR chunk");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t width, height;
|
||||||
|
if (!readBE32(pngFile, width) || !readBE32(pngFile, height)) return false;
|
||||||
|
|
||||||
|
uint8_t ihdrRest[5];
|
||||||
|
if (pngFile.read(ihdrRest, 5) != 5) return false;
|
||||||
|
|
||||||
|
uint8_t bitDepth = ihdrRest[0];
|
||||||
|
uint8_t colorType = ihdrRest[1];
|
||||||
|
uint8_t compression = ihdrRest[2];
|
||||||
|
uint8_t filter = ihdrRest[3];
|
||||||
|
uint8_t interlace = ihdrRest[4];
|
||||||
|
|
||||||
|
// Skip IHDR CRC
|
||||||
|
pngFile.seekCur(4);
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "Image: %ux%u, depth=%u, color=%u, interlace=%u", width, height, bitDepth, colorType, interlace);
|
||||||
|
|
||||||
|
if (compression != 0 || filter != 0) {
|
||||||
|
LOG_ERR("PNG", "Unsupported compression/filter method");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interlace != 0) {
|
||||||
|
LOG_ERR("PNG", "Interlaced PNGs not supported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety limits
|
||||||
|
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||||
|
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
||||||
|
|
||||||
|
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width == 0 || height == 0) {
|
||||||
|
LOG_ERR("PNG", "Image too large or zero (%ux%u)", width, height);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bytes per pixel and raw row bytes
|
||||||
|
uint8_t bytesPerPixel;
|
||||||
|
uint32_t rawRowBytes;
|
||||||
|
|
||||||
|
switch (colorType) {
|
||||||
|
case PNG_COLOR_GRAYSCALE:
|
||||||
|
if (bitDepth == 16) {
|
||||||
|
bytesPerPixel = 2;
|
||||||
|
rawRowBytes = width * 2;
|
||||||
|
} else if (bitDepth == 8) {
|
||||||
|
bytesPerPixel = 1;
|
||||||
|
rawRowBytes = width;
|
||||||
|
} else {
|
||||||
|
// Sub-byte: 1, 2, or 4 bits
|
||||||
|
bytesPerPixel = 1;
|
||||||
|
rawRowBytes = (width * bitDepth + 7) / 8;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_RGB:
|
||||||
|
bytesPerPixel = (bitDepth == 16) ? 6 : 3;
|
||||||
|
rawRowBytes = width * bytesPerPixel;
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_PALETTE:
|
||||||
|
bytesPerPixel = 1;
|
||||||
|
rawRowBytes = (width * bitDepth + 7) / 8;
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_GRAYSCALE_ALPHA:
|
||||||
|
bytesPerPixel = (bitDepth == 16) ? 4 : 2;
|
||||||
|
rawRowBytes = width * bytesPerPixel;
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_RGBA:
|
||||||
|
bytesPerPixel = (bitDepth == 16) ? 8 : 4;
|
||||||
|
rawRowBytes = width * bytesPerPixel;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG_ERR("PNG", "Unsupported color type: %d", colorType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate raw row bytes won't cause memory issues
|
||||||
|
if (rawRowBytes > 16384) {
|
||||||
|
LOG_ERR("PNG", "Row too large: %u bytes", rawRowBytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize decode context
|
||||||
|
PngDecodeContext ctx = {.file = pngFile,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.bitDepth = bitDepth,
|
||||||
|
.colorType = colorType,
|
||||||
|
.bytesPerPixel = bytesPerPixel,
|
||||||
|
.rawRowBytes = rawRowBytes,
|
||||||
|
.currentRow = nullptr,
|
||||||
|
.previousRow = nullptr,
|
||||||
|
.zstream = {},
|
||||||
|
.zstreamInitialized = false,
|
||||||
|
.chunkBytesRemaining = 0,
|
||||||
|
.idatFinished = false,
|
||||||
|
.readBuf = {},
|
||||||
|
.palette = {},
|
||||||
|
.paletteSize = 0};
|
||||||
|
|
||||||
|
// Allocate scanline buffers
|
||||||
|
ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes));
|
||||||
|
ctx.previousRow = static_cast<uint8_t*>(calloc(rawRowBytes, 1));
|
||||||
|
if (!ctx.currentRow || !ctx.previousRow) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate scanline buffers (%u bytes each)", rawRowBytes);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan for PLTE chunk (palette) and first IDAT chunk
|
||||||
|
// We need to read chunks until we find IDAT, collecting PLTE along the way
|
||||||
|
bool foundIdat = false;
|
||||||
|
while (!foundIdat) {
|
||||||
|
uint32_t chunkLen;
|
||||||
|
if (!readBE32(pngFile, chunkLen)) break;
|
||||||
|
|
||||||
|
uint8_t chunkType[4];
|
||||||
|
if (pngFile.read(chunkType, 4) != 4) break;
|
||||||
|
|
||||||
|
if (memcmp(chunkType, "PLTE", 4) == 0) {
|
||||||
|
int entries = chunkLen / 3;
|
||||||
|
if (entries > 256) entries = 256;
|
||||||
|
ctx.paletteSize = entries;
|
||||||
|
size_t palBytes = entries * 3;
|
||||||
|
pngFile.read(ctx.palette, palBytes);
|
||||||
|
// Skip any remaining palette data
|
||||||
|
if (chunkLen > palBytes) pngFile.seekCur(chunkLen - palBytes);
|
||||||
|
pngFile.seekCur(4); // CRC
|
||||||
|
} else if (memcmp(chunkType, "IDAT", 4) == 0) {
|
||||||
|
ctx.chunkBytesRemaining = chunkLen;
|
||||||
|
foundIdat = true;
|
||||||
|
} else if (memcmp(chunkType, "IEND", 4) == 0) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Skip unknown chunk
|
||||||
|
pngFile.seekCur(chunkLen + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundIdat) {
|
||||||
|
LOG_ERR("PNG", "No IDAT chunk found");
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize zlib decompression
|
||||||
|
memset(&ctx.zstream, 0, sizeof(ctx.zstream));
|
||||||
|
if (mz_inflateInit(&ctx.zstream) != MZ_OK) {
|
||||||
|
LOG_ERR("PNG", "Failed to initialize zlib");
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.zstreamInitialized = true;
|
||||||
|
|
||||||
|
// Calculate output dimensions (same logic as JpegToBmpConverter)
|
||||||
|
int outWidth = width;
|
||||||
|
int outHeight = height;
|
||||||
|
uint32_t scaleX_fp = 65536;
|
||||||
|
uint32_t scaleY_fp = 65536;
|
||||||
|
bool needsScaling = false;
|
||||||
|
|
||||||
|
if (targetWidth > 0 && targetHeight > 0 &&
|
||||||
|
(static_cast<int>(width) > targetWidth || static_cast<int>(height) > targetHeight)) {
|
||||||
|
const float scaleToFitWidth = static_cast<float>(targetWidth) / width;
|
||||||
|
const float scaleToFitHeight = static_cast<float>(targetHeight) / height;
|
||||||
|
float scale = 1.0;
|
||||||
|
if (crop) {
|
||||||
|
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
|
} else {
|
||||||
|
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
outWidth = static_cast<int>(width * scale);
|
||||||
|
outHeight = static_cast<int>(height * scale);
|
||||||
|
if (outWidth < 1) outWidth = 1;
|
||||||
|
if (outHeight < 1) outHeight = 1;
|
||||||
|
|
||||||
|
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth;
|
||||||
|
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight;
|
||||||
|
needsScaling = true;
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "Pre-scaling %ux%u -> %dx%d (fit to %dx%d)", width, height, outWidth, outHeight, targetWidth,
|
||||||
|
targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write BMP header
|
||||||
|
int bytesPerRow;
|
||||||
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
|
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
|
||||||
|
bytesPerRow = (outWidth + 3) / 4 * 4;
|
||||||
|
} else if (oneBit) {
|
||||||
|
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
|
||||||
|
bytesPerRow = (outWidth + 31) / 32 * 4;
|
||||||
|
} else {
|
||||||
|
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
|
||||||
|
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate BMP row buffer
|
||||||
|
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
|
if (!rowBuffer) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate row buffer");
|
||||||
|
mz_inflateEnd(&ctx.zstream);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ditherers (same as JpegToBmpConverter)
|
||||||
|
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
||||||
|
FloydSteinbergDitherer* fsDitherer = nullptr;
|
||||||
|
Atkinson1BitDitherer* atkinson1BitDitherer = nullptr;
|
||||||
|
|
||||||
|
if (oneBit) {
|
||||||
|
atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth);
|
||||||
|
} else if (!USE_8BIT_OUTPUT) {
|
||||||
|
if (USE_ATKINSON) {
|
||||||
|
atkinsonDitherer = new AtkinsonDitherer(outWidth);
|
||||||
|
} else if (USE_FLOYD_STEINBERG) {
|
||||||
|
fsDitherer = new FloydSteinbergDitherer(outWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scaling accumulators
|
||||||
|
uint32_t* rowAccum = nullptr;
|
||||||
|
uint16_t* rowCount = nullptr;
|
||||||
|
int currentOutY = 0;
|
||||||
|
uint32_t nextOutY_srcStart = 0;
|
||||||
|
|
||||||
|
if (needsScaling) {
|
||||||
|
rowAccum = new uint32_t[outWidth]();
|
||||||
|
rowCount = new uint16_t[outWidth]();
|
||||||
|
nextOutY_srcStart = scaleY_fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate grayscale row buffer - batch-convert each scanline to avoid
|
||||||
|
// per-pixel getPixelGray() switch overhead in the hot loops
|
||||||
|
auto* grayRow = static_cast<uint8_t*>(malloc(width));
|
||||||
|
if (!grayRow) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate grayscale row buffer");
|
||||||
|
delete[] rowAccum;
|
||||||
|
delete[] rowCount;
|
||||||
|
delete atkinsonDitherer;
|
||||||
|
delete fsDitherer;
|
||||||
|
delete atkinson1BitDitherer;
|
||||||
|
free(rowBuffer);
|
||||||
|
mz_inflateEnd(&ctx.zstream);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
// Process each scanline
|
||||||
|
for (uint32_t y = 0; y < height; y++) {
|
||||||
|
// Decode one scanline
|
||||||
|
if (!decodeScanline(ctx)) {
|
||||||
|
LOG_ERR("PNG", "Failed to decode scanline %u", y);
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-convert entire scanline to grayscale (one branch, tight loop)
|
||||||
|
convertScanlineToGray(ctx, grayRow);
|
||||||
|
|
||||||
|
if (!needsScaling) {
|
||||||
|
// Direct output (no scaling)
|
||||||
|
memset(rowBuffer, 0, bytesPerRow);
|
||||||
|
|
||||||
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
rowBuffer[x] = adjustPixel(grayRow[x]);
|
||||||
|
}
|
||||||
|
} else if (oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t bit =
|
||||||
|
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(grayRow[x], x) : quantize1bit(grayRow[x], x, y);
|
||||||
|
const int byteIndex = x / 8;
|
||||||
|
const int bitOffset = 7 - (x % 8);
|
||||||
|
rowBuffer[byteIndex] |= (bit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = adjustPixel(grayRow[x]);
|
||||||
|
uint8_t twoBit;
|
||||||
|
if (atkinsonDitherer) {
|
||||||
|
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||||
|
} else if (fsDitherer) {
|
||||||
|
twoBit = fsDitherer->processPixel(gray, x);
|
||||||
|
} else {
|
||||||
|
twoBit = quantize(gray, x, y);
|
||||||
|
}
|
||||||
|
const int byteIndex = (x * 2) / 8;
|
||||||
|
const int bitOffset = 6 - ((x * 2) % 8);
|
||||||
|
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinsonDitherer)
|
||||||
|
atkinsonDitherer->nextRow();
|
||||||
|
else if (fsDitherer)
|
||||||
|
fsDitherer->nextRow();
|
||||||
|
}
|
||||||
|
bmpOut.write(rowBuffer, bytesPerRow);
|
||||||
|
} else {
|
||||||
|
// Area-averaging scaling (same as JpegToBmpConverter)
|
||||||
|
for (int outX = 0; outX < outWidth; outX++) {
|
||||||
|
const int srcXStart = (static_cast<uint32_t>(outX) * scaleX_fp) >> 16;
|
||||||
|
const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16;
|
||||||
|
|
||||||
|
int sum = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (int srcX = srcXStart; srcX < srcXEnd && srcX < static_cast<int>(width); srcX++) {
|
||||||
|
sum += grayRow[srcX];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0 && srcXStart < static_cast<int>(width)) {
|
||||||
|
sum = grayRow[srcXStart];
|
||||||
|
count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowAccum[outX] += sum;
|
||||||
|
rowCount[outX] += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've crossed into the next output row
|
||||||
|
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
|
||||||
|
|
||||||
|
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
||||||
|
memset(rowBuffer, 0, bytesPerRow);
|
||||||
|
|
||||||
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||||
|
rowBuffer[x] = adjustPixel(gray);
|
||||||
|
}
|
||||||
|
} else if (oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||||
|
const uint8_t bit =
|
||||||
|
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, currentOutY);
|
||||||
|
const int byteIndex = x / 8;
|
||||||
|
const int bitOffset = 7 - (x % 8);
|
||||||
|
rowBuffer[byteIndex] |= (bit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
|
||||||
|
uint8_t twoBit;
|
||||||
|
if (atkinsonDitherer) {
|
||||||
|
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||||
|
} else if (fsDitherer) {
|
||||||
|
twoBit = fsDitherer->processPixel(gray, x);
|
||||||
|
} else {
|
||||||
|
twoBit = quantize(gray, x, currentOutY);
|
||||||
|
}
|
||||||
|
const int byteIndex = (x * 2) / 8;
|
||||||
|
const int bitOffset = 6 - ((x * 2) % 8);
|
||||||
|
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinsonDitherer)
|
||||||
|
atkinsonDitherer->nextRow();
|
||||||
|
else if (fsDitherer)
|
||||||
|
fsDitherer->nextRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
bmpOut.write(rowBuffer, bytesPerRow);
|
||||||
|
currentOutY++;
|
||||||
|
|
||||||
|
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
|
||||||
|
memset(rowCount, 0, outWidth * sizeof(uint16_t));
|
||||||
|
|
||||||
|
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap current/previous row buffers
|
||||||
|
uint8_t* temp = ctx.previousRow;
|
||||||
|
ctx.previousRow = ctx.currentRow;
|
||||||
|
ctx.currentRow = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
free(grayRow);
|
||||||
|
delete[] rowAccum;
|
||||||
|
delete[] rowCount;
|
||||||
|
delete atkinsonDitherer;
|
||||||
|
delete fsDitherer;
|
||||||
|
delete atkinson1BitDitherer;
|
||||||
|
free(rowBuffer);
|
||||||
|
mz_inflateEnd(&ctx.zstream);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
LOG_DBG("PNG", "Successfully converted PNG to BMP");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop) {
|
||||||
|
return pngFileToBmpStreamInternal(pngFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||||
|
int targetMaxHeight) {
|
||||||
|
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||||
|
int targetMaxHeight) {
|
||||||
|
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
|
||||||
|
}
|
||||||
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class FsFile;
|
||||||
|
class Print;
|
||||||
|
|
||||||
|
class PngToBmpConverter {
|
||||||
|
static bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit,
|
||||||
|
bool crop = true);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static bool pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop = true);
|
||||||
|
static bool pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||||
|
static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
Txt::Txt(std::string path, std::string cacheBasePath)
|
Txt::Txt(std::string path, std::string cacheBasePath)
|
||||||
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
|
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
|
||||||
@@ -15,14 +16,14 @@ bool Txt::load() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.exists(filepath.c_str())) {
|
if (!Storage.exists(filepath.c_str())) {
|
||||||
Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str());
|
LOG_ERR("TXT", "File does not exist: %s", filepath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
if (!Storage.openFileForRead("TXT", filepath, file)) {
|
||||||
Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str());
|
LOG_ERR("TXT", "Failed to open file: %s", filepath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ bool Txt::load() {
|
|||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [TXT] Loaded TXT file: %s (%zu bytes)\n", millis(), filepath.c_str(), fileSize);
|
LOG_DBG("TXT", "Loaded TXT file: %s (%zu bytes)", filepath.c_str(), fileSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +49,11 @@ std::string Txt::getTitle() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Txt::setupCacheDir() const {
|
void Txt::setupCacheDir() const {
|
||||||
if (!SdMan.exists(cacheBasePath.c_str())) {
|
if (!Storage.exists(cacheBasePath.c_str())) {
|
||||||
SdMan.mkdir(cacheBasePath.c_str());
|
Storage.mkdir(cacheBasePath.c_str());
|
||||||
}
|
}
|
||||||
if (!SdMan.exists(cachePath.c_str())) {
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
SdMan.mkdir(cachePath.c_str());
|
Storage.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +74,8 @@ std::string Txt::findCoverImage() const {
|
|||||||
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
|
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
|
||||||
for (const auto& ext : extensions) {
|
for (const auto& ext : extensions) {
|
||||||
std::string coverPath = folder + "/" + baseName + ext;
|
std::string coverPath = folder + "/" + baseName + ext;
|
||||||
if (SdMan.exists(coverPath.c_str())) {
|
if (Storage.exists(coverPath.c_str())) {
|
||||||
Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str());
|
LOG_DBG("TXT", "Found matching cover image: %s", coverPath.c_str());
|
||||||
return coverPath;
|
return coverPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,8 +85,8 @@ std::string Txt::findCoverImage() const {
|
|||||||
for (const auto& name : coverNames) {
|
for (const auto& name : coverNames) {
|
||||||
for (const auto& ext : extensions) {
|
for (const auto& ext : extensions) {
|
||||||
std::string coverPath = folder + "/" + std::string(name) + ext;
|
std::string coverPath = folder + "/" + std::string(name) + ext;
|
||||||
if (SdMan.exists(coverPath.c_str())) {
|
if (Storage.exists(coverPath.c_str())) {
|
||||||
Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str());
|
LOG_DBG("TXT", "Found fallback cover image: %s", coverPath.c_str());
|
||||||
return coverPath;
|
return coverPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,13 +99,13 @@ std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
|||||||
|
|
||||||
bool Txt::generateCoverBmp() const {
|
bool Txt::generateCoverBmp() const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string coverImagePath = findCoverImage();
|
std::string coverImagePath = findCoverImage();
|
||||||
if (coverImagePath.empty()) {
|
if (coverImagePath.empty()) {
|
||||||
Serial.printf("[%lu] [TXT] No cover image found for TXT file\n", millis());
|
LOG_DBG("TXT", "No cover image found for TXT file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +121,12 @@ bool Txt::generateCoverBmp() const {
|
|||||||
|
|
||||||
if (isBmp) {
|
if (isBmp) {
|
||||||
// Copy BMP file to cache
|
// Copy BMP file to cache
|
||||||
Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis());
|
LOG_DBG("TXT", "Copying BMP cover image to cache");
|
||||||
FsFile src, dst;
|
FsFile src, dst;
|
||||||
if (!SdMan.openFileForRead("TXT", coverImagePath, src)) {
|
if (!Storage.openFileForRead("TXT", coverImagePath, src)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
||||||
src.close();
|
src.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,18 +137,18 @@ bool Txt::generateCoverBmp() const {
|
|||||||
}
|
}
|
||||||
src.close();
|
src.close();
|
||||||
dst.close();
|
dst.close();
|
||||||
Serial.printf("[%lu] [TXT] Copied BMP cover to cache\n", millis());
|
LOG_DBG("TXT", "Copied BMP cover to cache");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isJpg) {
|
if (isJpg) {
|
||||||
// Convert JPG/JPEG to BMP (same approach as Epub)
|
// Convert JPG/JPEG to BMP (same approach as Epub)
|
||||||
Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis());
|
LOG_DBG("TXT", "Generating BMP from JPG cover image");
|
||||||
FsFile coverJpg, coverBmp;
|
FsFile coverJpg, coverBmp;
|
||||||
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
if (!Storage.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -156,16 +157,16 @@ bool Txt::generateCoverBmp() const {
|
|||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis());
|
LOG_ERR("TXT", "Failed to generate BMP from JPG cover image");
|
||||||
SdMan.remove(getCoverBmpPath().c_str());
|
Storage.remove(getCoverBmpPath().c_str());
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis());
|
LOG_DBG("TXT", "Generated BMP from JPG cover image");
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PNG files are not supported (would need a PNG decoder)
|
// PNG files are not supported (would need a PNG decoder)
|
||||||
Serial.printf("[%lu] [TXT] Cover image format not supported (only BMP/JPG/JPEG)\n", millis());
|
LOG_ERR("TXT", "Cover image format not supported (only BMP/JPG/JPEG)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
if (!Storage.openFileForRead("TXT", filepath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
|
|||||||
|
|
||||||
return cp;
|
return cp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t utf8RemoveLastChar(std::string& str) {
|
||||||
|
if (str.empty()) return 0;
|
||||||
|
size_t pos = str.size() - 1;
|
||||||
|
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
||||||
|
--pos;
|
||||||
|
}
|
||||||
|
str.resize(pos);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate string by removing N UTF-8 characters from the end
|
||||||
|
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
||||||
|
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
||||||
|
utf8RemoveLastChar(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#define REPLACEMENT_GLYPH 0xFFFD
|
#define REPLACEMENT_GLYPH 0xFFFD
|
||||||
|
|
||||||
uint32_t utf8NextCodepoint(const unsigned char** string);
|
uint32_t utf8NextCodepoint(const unsigned char** string);
|
||||||
|
// Remove the last UTF-8 codepoint from a std::string and return the new size.
|
||||||
|
size_t utf8RemoveLastChar(std::string& str);
|
||||||
|
// Truncate string by removing N UTF-8 codepoints from the end.
|
||||||
|
void utf8TruncateChars(std::string& str, size_t numChars);
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
#include "Xtc.h"
|
#include "Xtc.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
bool Xtc::load() {
|
bool Xtc::load() {
|
||||||
Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str());
|
LOG_DBG("XTC", "Loading XTC: %s", filepath.c_str());
|
||||||
|
|
||||||
// Initialize parser
|
// Initialize parser
|
||||||
parser.reset(new xtc::XtcParser());
|
parser.reset(new xtc::XtcParser());
|
||||||
@@ -19,43 +19,43 @@ bool Xtc::load() {
|
|||||||
// Open XTC file
|
// Open XTC file
|
||||||
xtc::XtcError err = parser->open(filepath.c_str());
|
xtc::XtcError err = parser->open(filepath.c_str());
|
||||||
if (err != xtc::XtcError::OK) {
|
if (err != xtc::XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to load: %s\n", millis(), xtc::errorToString(err));
|
LOG_ERR("XTC", "Failed to load: %s", xtc::errorToString(err));
|
||||||
parser.reset();
|
parser.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [XTC] Loaded XTC: %s (%lu pages)\n", millis(), filepath.c_str(), parser->getPageCount());
|
LOG_DBG("XTC", "Loaded XTC: %s (%lu pages)", filepath.c_str(), parser->getPageCount());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Xtc::clearCache() const {
|
bool Xtc::clearCache() const {
|
||||||
if (!SdMan.exists(cachePath.c_str())) {
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [XTC] Cache does not exist, no action needed\n", millis());
|
LOG_DBG("XTC", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
if (!Storage.removeDir(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to clear cache\n", millis());
|
LOG_ERR("XTC", "Failed to clear cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Cache cleared successfully\n", millis());
|
LOG_DBG("XTC", "Cache cleared successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Xtc::setupCacheDir() const {
|
void Xtc::setupCacheDir() const {
|
||||||
if (SdMan.exists(cachePath.c_str())) {
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directories recursively
|
// Create directories recursively
|
||||||
for (size_t i = 1; i < cachePath.length(); i++) {
|
for (size_t i = 1; i < cachePath.length(); i++) {
|
||||||
if (cachePath[i] == '/') {
|
if (cachePath[i] == '/') {
|
||||||
SdMan.mkdir(cachePath.substr(0, i).c_str());
|
Storage.mkdir(cachePath.substr(0, i).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SdMan.mkdir(cachePath.c_str());
|
Storage.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Xtc::getTitle() const {
|
std::string Xtc::getTitle() const {
|
||||||
@@ -114,17 +114,17 @@ std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
|||||||
|
|
||||||
bool Xtc::generateCoverBmp() const {
|
bool Xtc::generateCoverBmp() const {
|
||||||
// Already generated
|
// Already generated
|
||||||
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded || !parser) {
|
if (!loaded || !parser) {
|
||||||
Serial.printf("[%lu] [XTC] Cannot generate cover BMP, file not loaded\n", millis());
|
LOG_ERR("XTC", "Cannot generate cover BMP, file not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser->getPageCount() == 0) {
|
if (parser->getPageCount() == 0) {
|
||||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
LOG_ERR("XTC", "No pages in XTC file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
// Get first page info for cover
|
// Get first page info for cover
|
||||||
xtc::PageInfo pageInfo;
|
xtc::PageInfo pageInfo;
|
||||||
if (!parser->getPageInfo(0, pageInfo)) {
|
if (!parser->getPageInfo(0, pageInfo)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
LOG_DBG("XTC", "Failed to get first page info");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,22 +152,22 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
}
|
}
|
||||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||||
if (!pageBuffer) {
|
if (!pageBuffer) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
LOG_ERR("XTC", "Failed to allocate page buffer (%lu bytes)", bitmapSize);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load first page (cover)
|
// Load first page (cover)
|
||||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||||
if (bytesRead == 0) {
|
if (bytesRead == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to load cover page\n", millis());
|
LOG_ERR("XTC", "Failed to load cover page");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create BMP file
|
// Create BMP file
|
||||||
FsFile coverBmp;
|
FsFile coverBmp;
|
||||||
if (!SdMan.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) {
|
if (!Storage.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to create cover BMP file\n", millis());
|
LOG_DBG("XTC", "Failed to create cover BMP file");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -297,25 +297,26 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generated cover BMP: %s\n", millis(), getCoverBmpPath().c_str());
|
LOG_DBG("XTC", "Generated cover BMP: %s", getCoverBmpPath().c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||||
|
std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||||
|
|
||||||
bool Xtc::generateThumbBmp() const {
|
bool Xtc::generateThumbBmp(int height) const {
|
||||||
// Already generated
|
// Already generated
|
||||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded || !parser) {
|
if (!loaded || !parser) {
|
||||||
Serial.printf("[%lu] [XTC] Cannot generate thumb BMP, file not loaded\n", millis());
|
LOG_ERR("XTC", "Cannot generate thumb BMP, file not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser->getPageCount() == 0) {
|
if (parser->getPageCount() == 0) {
|
||||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
LOG_ERR("XTC", "No pages in XTC file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +326,7 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
// Get first page info for cover
|
// Get first page info for cover
|
||||||
xtc::PageInfo pageInfo;
|
xtc::PageInfo pageInfo;
|
||||||
if (!parser->getPageInfo(0, pageInfo)) {
|
if (!parser->getPageInfo(0, pageInfo)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
LOG_DBG("XTC", "Failed to get first page info");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,13 +334,13 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
const uint8_t bitDepth = parser->getBitDepth();
|
const uint8_t bitDepth = parser->getBitDepth();
|
||||||
|
|
||||||
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card)
|
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card)
|
||||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
int THUMB_TARGET_HEIGHT = height;
|
||||||
|
|
||||||
// Calculate scale factor
|
// Calculate scale factor
|
||||||
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
|
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
|
||||||
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
|
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
|
||||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
float scale = (scaleX > scaleY) ? scaleX : scaleY; // for cropping
|
||||||
|
|
||||||
// Only scale down, never up
|
// Only scale down, never up
|
||||||
if (scale >= 1.0f) {
|
if (scale >= 1.0f) {
|
||||||
@@ -347,8 +348,8 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
// Copy cover.bmp to thumb.bmp
|
// Copy cover.bmp to thumb.bmp
|
||||||
if (generateCoverBmp()) {
|
if (generateCoverBmp()) {
|
||||||
FsFile src, dst;
|
FsFile src, dst;
|
||||||
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
if (Storage.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
||||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) {
|
if (Storage.openFileForWrite("XTC", getThumbBmpPath(height), dst)) {
|
||||||
uint8_t buffer[512];
|
uint8_t buffer[512];
|
||||||
while (src.available()) {
|
while (src.available()) {
|
||||||
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
||||||
@@ -358,8 +359,8 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
}
|
}
|
||||||
src.close();
|
src.close();
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
|
LOG_DBG("XTC", "Copied cover to thumb (no scaling needed)");
|
||||||
return SdMan.exists(getThumbBmpPath().c_str());
|
return Storage.exists(getThumbBmpPath(height).c_str());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -367,8 +368,8 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
||||||
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
LOG_DBG("XTC", "Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)", pageInfo.width, pageInfo.height, thumbWidth,
|
||||||
pageInfo.height, thumbWidth, thumbHeight, scale);
|
thumbHeight, scale);
|
||||||
|
|
||||||
// Allocate buffer for page data
|
// Allocate buffer for page data
|
||||||
size_t bitmapSize;
|
size_t bitmapSize;
|
||||||
@@ -379,22 +380,22 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
}
|
}
|
||||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||||
if (!pageBuffer) {
|
if (!pageBuffer) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
LOG_ERR("XTC", "Failed to allocate page buffer (%lu bytes)", bitmapSize);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load first page (cover)
|
// Load first page (cover)
|
||||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||||
if (bytesRead == 0) {
|
if (bytesRead == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to load cover page for thumb\n", millis());
|
LOG_ERR("XTC", "Failed to load cover page for thumb");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
||||||
FsFile thumbBmp;
|
FsFile thumbBmp;
|
||||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) {
|
if (!Storage.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
LOG_DBG("XTC", "Failed to create thumb BMP file");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -557,8 +558,7 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
|
LOG_DBG("XTC", "Generated thumb BMP (%dx%d): %s", thumbWidth, thumbHeight, getThumbBmpPath(height).c_str());
|
||||||
getThumbBmpPath().c_str());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ class Xtc {
|
|||||||
bool generateCoverBmp() const;
|
bool generateCoverBmp() const;
|
||||||
// Thumbnail support (for Continue Reading card)
|
// Thumbnail support (for Continue Reading card)
|
||||||
std::string getThumbBmpPath() const;
|
std::string getThumbBmpPath() const;
|
||||||
bool generateThumbBmp() const;
|
std::string getThumbBmpPath(int height) const;
|
||||||
|
bool generateThumbBmp(int height) const;
|
||||||
|
|
||||||
// Page access
|
// Page access
|
||||||
uint32_t getPageCount() const;
|
uint32_t getPageCount() const;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#include "XtcParser.h"
|
#include "XtcParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open file
|
// Open file
|
||||||
if (!SdMan.openFileForRead("XTC", filepath, m_file)) {
|
if (!Storage.openFileForRead("XTC", filepath, m_file)) {
|
||||||
m_lastError = XtcError::FILE_NOT_FOUND;
|
m_lastError = XtcError::FILE_NOT_FOUND;
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
// Read header
|
// Read header
|
||||||
m_lastError = readHeader();
|
m_lastError = readHeader();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read header: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read header: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -51,13 +51,13 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
if (m_header.hasMetadata) {
|
if (m_header.hasMetadata) {
|
||||||
m_lastError = readTitle();
|
m_lastError = readTitle();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read title: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
m_lastError = readAuthor();
|
m_lastError = readAuthor();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read author: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
// Read page table
|
// Read page table
|
||||||
m_lastError = readPageTable();
|
m_lastError = readPageTable();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read page table: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read page table: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -74,14 +74,13 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
// Read chapters if present
|
// Read chapters if present
|
||||||
m_lastError = readChapters();
|
m_lastError = readChapters();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read chapters: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read chapters: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_isOpen = true;
|
m_isOpen = true;
|
||||||
Serial.printf("[%lu] [XTC] Opened file: %s (%u pages, %dx%d)\n", millis(), filepath, m_header.pageCount,
|
LOG_DBG("XTC", "Opened file: %s (%u pages, %dx%d)", filepath, m_header.pageCount, m_defaultWidth, m_defaultHeight);
|
||||||
m_defaultWidth, m_defaultHeight);
|
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +105,7 @@ XtcError XtcParser::readHeader() {
|
|||||||
|
|
||||||
// Verify magic number (accept both XTC and XTCH)
|
// Verify magic number (accept both XTC and XTCH)
|
||||||
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
||||||
Serial.printf("[%lu] [XTC] Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n", millis(), m_header.magic,
|
LOG_DBG("XTC", "Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)", m_header.magic, XTC_MAGIC, XTCH_MAGIC);
|
||||||
XTC_MAGIC, XTCH_MAGIC);
|
|
||||||
return XtcError::INVALID_MAGIC;
|
return XtcError::INVALID_MAGIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +118,7 @@ XtcError XtcParser::readHeader() {
|
|||||||
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
||||||
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
||||||
if (!validVersion) {
|
if (!validVersion) {
|
||||||
Serial.printf("[%lu] [XTC] Unsupported version: %u.%u\n", millis(), m_header.versionMajor, m_header.versionMinor);
|
LOG_DBG("XTC", "Unsupported version: %u.%u", m_header.versionMajor, m_header.versionMinor);
|
||||||
return XtcError::INVALID_VERSION;
|
return XtcError::INVALID_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +127,9 @@ XtcError XtcParser::readHeader() {
|
|||||||
return XtcError::CORRUPTED_HEADER;
|
return XtcError::CORRUPTED_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
LOG_DBG("XTC", "Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u", m_header.magic,
|
||||||
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
||||||
m_header.pageCount, m_bitDepth);
|
m_header.pageCount, m_bitDepth);
|
||||||
|
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
@@ -146,7 +144,7 @@ XtcError XtcParser::readTitle() {
|
|||||||
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
||||||
m_title = titleBuf;
|
m_title = titleBuf;
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
|
LOG_DBG("XTC", "Title: %s", m_title.c_str());
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,19 +159,19 @@ XtcError XtcParser::readAuthor() {
|
|||||||
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
||||||
m_author = authorBuf;
|
m_author = authorBuf;
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
LOG_DBG("XTC", "Author: %s", m_author.c_str());
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
XtcError XtcParser::readPageTable() {
|
XtcError XtcParser::readPageTable() {
|
||||||
if (m_header.pageTableOffset == 0) {
|
if (m_header.pageTableOffset == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
LOG_DBG("XTC", "Page table offset is 0, cannot read");
|
||||||
return XtcError::CORRUPTED_HEADER;
|
return XtcError::CORRUPTED_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek to page table
|
// Seek to page table
|
||||||
if (!m_file.seek(m_header.pageTableOffset)) {
|
if (!m_file.seek(m_header.pageTableOffset)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to seek to page table at %llu\n", millis(), m_header.pageTableOffset);
|
LOG_DBG("XTC", "Failed to seek to page table at %llu", m_header.pageTableOffset);
|
||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +182,7 @@ XtcError XtcParser::readPageTable() {
|
|||||||
PageTableEntry entry;
|
PageTableEntry entry;
|
||||||
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
||||||
if (bytesRead != sizeof(PageTableEntry)) {
|
if (bytesRead != sizeof(PageTableEntry)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read page table entry %u\n", millis(), i);
|
LOG_DBG("XTC", "Failed to read page table entry %u", i);
|
||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +199,7 @@ XtcError XtcParser::readPageTable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Read %u page table entries\n", millis(), m_header.pageCount);
|
LOG_DBG("XTC", "Read %u page table entries", m_header.pageCount);
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +305,7 @@ XtcError XtcParser::readChapters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_hasChapters = !m_chapters.empty();
|
m_hasChapters = !m_chapters.empty();
|
||||||
Serial.printf("[%lu] [XTC] Chapters: %u\n", millis(), static_cast<unsigned int>(m_chapters.size()));
|
LOG_DBG("XTC", "Chapters: %u", static_cast<unsigned int>(m_chapters.size()));
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +332,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
|
|
||||||
// Seek to page data
|
// Seek to page data
|
||||||
if (!m_file.seek(page.offset)) {
|
if (!m_file.seek(page.offset)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset);
|
LOG_DBG("XTC", "Failed to seek to page %u at offset %lu", pageIndex, page.offset);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -343,7 +341,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
XtgPageHeader pageHeader;
|
XtgPageHeader pageHeader;
|
||||||
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
||||||
if (headerRead != sizeof(XtgPageHeader)) {
|
if (headerRead != sizeof(XtgPageHeader)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read page header for page %u\n", millis(), pageIndex);
|
LOG_DBG("XTC", "Failed to read page header for page %u", pageIndex);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -351,8 +349,8 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
||||||
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
||||||
if (pageHeader.magic != expectedMagic) {
|
if (pageHeader.magic != expectedMagic) {
|
||||||
Serial.printf("[%lu] [XTC] Invalid page magic for page %u: 0x%08X (expected 0x%08X)\n", millis(), pageIndex,
|
LOG_DBG("XTC", "Invalid page magic for page %u: 0x%08X (expected 0x%08X)", pageIndex, pageHeader.magic,
|
||||||
pageHeader.magic, expectedMagic);
|
expectedMagic);
|
||||||
m_lastError = XtcError::INVALID_MAGIC;
|
m_lastError = XtcError::INVALID_MAGIC;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -370,7 +368,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
|
|
||||||
// Check buffer size
|
// Check buffer size
|
||||||
if (bufferSize < bitmapSize) {
|
if (bufferSize < bitmapSize) {
|
||||||
Serial.printf("[%lu] [XTC] Buffer too small: need %u, have %u\n", millis(), bitmapSize, bufferSize);
|
LOG_DBG("XTC", "Buffer too small: need %u, have %u", bitmapSize, bufferSize);
|
||||||
m_lastError = XtcError::MEMORY_ERROR;
|
m_lastError = XtcError::MEMORY_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -378,7 +376,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
// Read bitmap data
|
// Read bitmap data
|
||||||
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
||||||
if (bytesRead != bitmapSize) {
|
if (bytesRead != bitmapSize) {
|
||||||
Serial.printf("[%lu] [XTC] Page read error: expected %u, got %u\n", millis(), bitmapSize, bytesRead);
|
LOG_DBG("XTC", "Page read error: expected %u, got %u", bitmapSize, bytesRead);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -444,7 +442,7 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
|||||||
|
|
||||||
bool XtcParser::isValidXtcFile(const char* filepath) {
|
bool XtcParser::isValidXtcFile(const char* filepath) {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("XTC", filepath, file)) {
|
if (!Storage.openFileForRead("XTC", filepath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "ZipFile.h"
|
#include "ZipFile.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <miniz.h>
|
#include <miniz.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -10,7 +10,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
|||||||
// Setup inflator
|
// Setup inflator
|
||||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||||
@@ -23,7 +23,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
|||||||
free(inflator);
|
free(inflator);
|
||||||
|
|
||||||
if (status != TINFL_STATUS_DONE) {
|
if (status != TINFL_STATUS_DONE) {
|
||||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,13 +195,13 @@ long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (read != localHeaderSize) {
|
if (read != localHeaderSize) {
|
||||||
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
|
LOG_ERR("ZIP", "Something went wrong reading the local header");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||||
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||||
Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis());
|
LOG_ERR("ZIP", "Not a valid zip file header");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
|
|
||||||
const size_t fileSize = file.size();
|
const size_t fileSize = file.size();
|
||||||
if (fileSize < 22) {
|
if (fileSize < 22) {
|
||||||
Serial.printf("[%lu] [ZIP] File too small to be a valid zip\n", millis());
|
LOG_ERR("ZIP", "File too small to be a valid zip");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -234,7 +234,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
||||||
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for EOCD scan buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (foundOffset == -1) {
|
if (foundOffset == -1) {
|
||||||
Serial.printf("[%lu] [ZIP] EOCD signature not found in zip file\n", millis());
|
LOG_ERR("ZIP", "EOCD signature not found in zip file");
|
||||||
free(buffer);
|
free(buffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -279,7 +279,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ZipFile::open() {
|
bool ZipFile::open() {
|
||||||
if (!SdMan.openFileForRead("ZIP", filePath, file)) {
|
if (!Storage.openFileForRead("ZIP", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -407,7 +407,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
||||||
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
||||||
if (data == nullptr) {
|
if (data == nullptr) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
|
LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -422,7 +422,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataRead != inflatedDataSize) {
|
if (dataRead != inflatedDataSize) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
|
LOG_ERR("ZIP", "Failed to read data");
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -432,7 +432,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
// Read out deflated content from file
|
// Read out deflated content from file
|
||||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||||
if (deflatedData == nullptr) {
|
if (deflatedData == nullptr) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -445,7 +445,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataRead != deflatedDataSize) {
|
if (dataRead != deflatedDataSize) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
|
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead);
|
||||||
free(deflatedData);
|
free(deflatedData);
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -455,14 +455,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
free(deflatedData);
|
free(deflatedData);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis());
|
LOG_ERR("ZIP", "Failed to inflate file");
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue out of block with data set
|
// Continue out of block with data set
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
LOG_ERR("ZIP", "Unsupported compression method");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -498,7 +498,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// no deflation, just read content
|
// no deflation, just read content
|
||||||
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for buffer");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -509,7 +509,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
while (remaining > 0) {
|
while (remaining > 0) {
|
||||||
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
||||||
if (dataRead == 0) {
|
if (dataRead == 0) {
|
||||||
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
|
LOG_ERR("ZIP", "Could not read more bytes");
|
||||||
free(buffer);
|
free(buffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -532,7 +532,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Setup inflator
|
// Setup inflator
|
||||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -544,7 +544,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Setup file read buffer
|
// Setup file read buffer
|
||||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!fileReadBuffer) {
|
if (!fileReadBuffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
||||||
free(inflator);
|
free(inflator);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -554,7 +554,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
|
|
||||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||||
if (!outputBuffer) {
|
if (!outputBuffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for dictionary");
|
||||||
free(inflator);
|
free(inflator);
|
||||||
free(fileReadBuffer);
|
free(fileReadBuffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
@@ -605,7 +605,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
if (outBytes > 0) {
|
if (outBytes > 0) {
|
||||||
processedOutputBytes += outBytes;
|
processedOutputBytes += outBytes;
|
||||||
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
|
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
|
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -619,7 +619,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status < 0) {
|
if (status < 0) {
|
||||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -630,8 +630,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status == TINFL_STATUS_DONE) {
|
if (status == TINFL_STATUS_DONE) {
|
||||||
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
|
LOG_ERR("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
|
||||||
inflatedDataSize);
|
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -643,7 +642,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, EOF reached without TINFL_STATUS_DONE
|
// If we get here, EOF reached without TINFL_STATUS_DONE
|
||||||
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
|
LOG_ERR("ZIP", "Unexpected EOF");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -657,6 +656,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
LOG_ERR("ZIP", "Unsupported compression method");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user