Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e04eec072 | ||
|
|
def1094411 | ||
|
|
7538e55795 | ||
|
|
21e7d29286 | ||
|
|
5e52a46837 | ||
|
|
6909f127b4 | ||
|
|
4f0a3aa4dd | ||
|
|
bb983d0ef4 | ||
|
|
b45eaf7ded | ||
|
|
75b0ed7781 | ||
|
|
47f3137dee | ||
|
|
d8632eae08 | ||
|
|
3223e85ea5 | ||
|
|
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 | ||
|
|
ebcd813ff6 | ||
|
|
712c566664 | ||
|
|
5894ae5afe | ||
|
|
8c1c80787a | ||
|
|
140fcb9db5 | ||
|
|
e0b6b9b28a | ||
|
|
83315b6179 | ||
|
|
8e0d2bece2 | ||
|
|
4848a77e1b | ||
|
|
49190cca6d | ||
|
|
e9c2fe1c87 | ||
|
|
dd1741bf0b | ||
|
|
51c5c3c0aa | ||
|
|
5e24895f6d | ||
|
|
e2ca0e94ca | ||
|
|
a4b9a43ca1 | ||
|
|
c73fca26f5 | ||
|
|
dfd7b615dc | ||
|
|
aca6dceaa8 | ||
|
|
6ca75c4653 | ||
|
|
1b9c8ab545 | ||
|
|
bf6cf83577 | ||
|
|
3a761b18af | ||
|
|
13f0ebed96 | ||
|
|
0bc0baa966 | ||
|
|
5d369df6be | ||
|
|
b8ebcf5867 | ||
|
|
e858ebbe88 | ||
|
|
9224bc3f8c | ||
|
|
67a679ab41 | ||
|
|
7a53342f9d |
+89
-17
@@ -1,10 +1,55 @@
|
||||
name: CI
|
||||
'on':
|
||||
name: CI (build)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -19,19 +64,46 @@ jobs:
|
||||
- name: Install PlatformIO Core
|
||||
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
|
||||
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,6 +1,7 @@
|
||||
name: "PR Formatting"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
@@ -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
|
||||
+4
-1
@@ -4,5 +4,8 @@
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
|
||||
@@ -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.
|
||||
@@ -41,7 +41,11 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
- [ ] Full UTF support
|
||||
- [x] Screen rotation
|
||||
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
|
||||
|
||||
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
|
||||
|
||||
@@ -56,7 +60,7 @@ back to the other partition using the "Swap boot partition" button here https://
|
||||
### Web (specific firmware version)
|
||||
|
||||
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
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
@@ -80,7 +84,7 @@ See [Development](#development) below.
|
||||
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:
|
||||
git submodule update --init --recursive
|
||||
@@ -93,6 +97,25 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
|
||||
```sh
|
||||
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
|
||||
|
||||
@@ -131,9 +154,12 @@ For more details on the internal file structures, see the [file formats document
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
1. Fork the repo
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
|
||||
### 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 Reader Features:** No highlighting, notes, or dictionary lookup. 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!
|
||||
+50
-2
@@ -13,14 +13,17 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [3.2 Book Selection](#32-book-selection)
|
||||
- [3.3 Reading Mode](#33-reading-mode)
|
||||
- [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.6 Sleep Screen](#36-sleep-screen)
|
||||
- [4. Reading Mode](#4-reading-mode)
|
||||
- [Page Turning](#page-turning)
|
||||
- [Chapter Navigation](#chapter-navigation)
|
||||
- [System Navigation](#system-navigation)
|
||||
- [Supported Languages](#supported-languages)
|
||||
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
|
||||
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||
|
||||
|
||||
## 1. Hardware Overview
|
||||
@@ -81,6 +84,18 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con
|
||||
> [!TIP]
|
||||
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
|
||||
|
||||
### 3.4.1 Calibre Wireless Transfers
|
||||
|
||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||
|
||||
1. Install the plugin in Calibre:
|
||||
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||
- Download the zip file.
|
||||
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||
2. On the device: File Transfer → Connect to Calibre → Join a network.
|
||||
3. Make sure your computer is on the same WiFi network.
|
||||
4. In Calibre, click "Send to device" to transfer books.
|
||||
|
||||
### 3.5 Settings
|
||||
|
||||
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
||||
@@ -90,13 +105,21 @@ 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
|
||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||
- "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:
|
||||
- "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)
|
||||
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
|
||||
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
|
||||
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
|
||||
- **Status Bar**: Configure the status bar displayed while reading:
|
||||
- "None" - No status bar
|
||||
- "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:
|
||||
- "Never" - Always show battery percentage (default)
|
||||
- "In Reader" - Show battery percentage everywhere except in reading mode
|
||||
@@ -116,6 +139,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- Back, Confirm, Left, Right (default)
|
||||
- Left, Right, Back, Confirm
|
||||
- Left, Back, Confirm, Right
|
||||
- Back, Confirm, Right, Left
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
|
||||
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||
@@ -131,7 +155,10 @@ 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".
|
||||
- **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.
|
||||
- **Calibre Settings**: Set up integration for accessing a Calibre web library or connecting to Calibre as a wireless device.
|
||||
- **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.
|
||||
- **Check for updates**: Check for firmware updates over WiFi.
|
||||
|
||||
### 3.6 Sleep Screen
|
||||
@@ -177,6 +204,15 @@ This feature can be disabled in **[Settings](#35-settings)** to help avoid chang
|
||||
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages:
|
||||
|
||||
* **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.
|
||||
|
||||
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chapter Selection Screen
|
||||
@@ -194,3 +230,15 @@ Accessible by pressing **Confirm** while inside a book.
|
||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||
|
||||
* **Images:** Embedded images in e-books will not render.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ Click **File Manager** to access file management features.
|
||||
|
||||
1. Click the **+ Add** button in the top-right corner
|
||||
2. Select **New Folder** from the dropdown menu
|
||||
3. Enter a folder name (letters, numbers, underscores, and hyphens only)
|
||||
3. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
4. Click **Create Folder**
|
||||
|
||||
This is useful for organizing your ebooks by genre, author, or series.
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
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;
|
||||
}
|
||||
if (style == ITALIC && italic) {
|
||||
} else if (hasItalic && italic) {
|
||||
return italic;
|
||||
}
|
||||
if (style == BOLD_ITALIC) {
|
||||
if (boldItalic) {
|
||||
return boldItalic;
|
||||
}
|
||||
if (bold) {
|
||||
return bold;
|
||||
}
|
||||
if (italic) {
|
||||
return italic;
|
||||
}
|
||||
}
|
||||
|
||||
return regular;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
class EpdFontFamily {
|
||||
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,
|
||||
const EpdFont* boldItalic = nullptr)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_bold
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_bold 12 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_bolditalic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_bolditalic 12 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_italic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_italic 12 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_regular
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_regular 12 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_bold
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_bold 14 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_bolditalic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_bolditalic 14 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_italic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_italic 14 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_regular
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_regular 14 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_bold
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_bold 16 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_bolditalic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_bolditalic 16 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_italic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_italic 16 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_regular
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_regular 16 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_bold
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_bold 18 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_bolditalic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_bolditalic 18 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_italic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_italic 18 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_regular
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_regular 18 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_12_bold
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_bold 12 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_12_bolditalic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_bolditalic 12 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_12_italic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_italic 12 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_12_regular
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_regular 12 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_14_bold
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_bold 14 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_14_bolditalic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_bolditalic 14 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_14_italic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_italic 14 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_14_regular
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_regular 14 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_16_bold
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_bold 16 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_16_bolditalic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_bolditalic 16 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_16_italic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_italic 16 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_16_regular
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_18_bold
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_bold 18 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_18_bolditalic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_bolditalic 18 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_18_italic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_italic 18 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_18_regular
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_regular 18 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: notosans_8_regular
|
||||
* size: 8
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_bold
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_bold 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_bolditalic
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_bolditalic 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_italic
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_italic 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_regular
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_regular 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_bold
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_bold 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_bolditalic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_bolditalic 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_italic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_italic 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_regular
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_regular 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_bold
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_bold 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_bolditalic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_bolditalic 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_italic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_italic 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_regular
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_regular 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_bold
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_bold 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_bolditalic
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_bolditalic 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_italic
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_italic 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_regular
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_regular 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: ubuntu_10_bold
|
||||
* size: 10
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_10_bold 10 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: ubuntu_10_regular
|
||||
* size: 10
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_10_regular 10 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: ubuntu_12_bold
|
||||
* size: 12
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_12_bold 12 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* name: ubuntu_12_regular
|
||||
* size: 12
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_12_regular 12 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@@ -270,9 +270,17 @@ for index, glyph in enumerate(all_glyphs):
|
||||
glyph_data.extend([b for b in packed])
|
||||
glyph_props.append(props)
|
||||
|
||||
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
|
||||
print("#pragma once")
|
||||
print("#include \"EpdFontData.h\"\n")
|
||||
print(f"""/**
|
||||
* generated by fontconvert.py
|
||||
* name: {font_name}
|
||||
* size: {size}
|
||||
* mode: {'2-bit' if is2Bit else '1-bit'}
|
||||
* Command used: {' '.join(sys.argv)}
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
""")
|
||||
|
||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||
for c in chunks(glyph_data, 16):
|
||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||
|
||||
+112
-16
@@ -86,6 +86,10 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
tocNavItem = opfParser.tocNavPath;
|
||||
}
|
||||
|
||||
if (!opfParser.cssFiles.empty()) {
|
||||
cssFiles = opfParser.cssFiles;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
return true;
|
||||
}
|
||||
@@ -204,15 +208,91 @@ bool Epub::parseTocNavFile() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Epub::getCssRulesCache() const { return cachePath + "/css_rules.cache"; }
|
||||
|
||||
bool Epub::loadCssRulesFromCache() const {
|
||||
FsFile cssCacheFile;
|
||||
if (SdMan.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||
if (cssParser->loadFromCache(cssCacheFile)) {
|
||||
cssCacheFile.close();
|
||||
Serial.printf("[%lu] [EBP] Loaded CSS rules from cache\n", millis());
|
||||
return true;
|
||||
}
|
||||
cssCacheFile.close();
|
||||
Serial.printf("[%lu] [EBP] CSS cache invalid, reparsing\n", millis());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Epub::parseCssFiles() const {
|
||||
if (cssFiles.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis());
|
||||
}
|
||||
|
||||
// Try to load from CSS cache first
|
||||
if (!loadCssRulesFromCache()) {
|
||||
// Cache miss - parse CSS files
|
||||
for (const auto& cssPath : cssFiles) {
|
||||
Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str());
|
||||
|
||||
// Extract CSS file to temp location
|
||||
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||
FsFile tempCssFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||
Serial.printf("[%lu] [EBP] Could not create temp CSS file\n", millis());
|
||||
continue;
|
||||
}
|
||||
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read CSS file: %s\n", millis(), cssPath.c_str());
|
||||
tempCssFile.close();
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
tempCssFile.close();
|
||||
|
||||
// Parse the CSS file
|
||||
if (!SdMan.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||
Serial.printf("[%lu] [EBP] Could not open temp CSS file for reading\n", millis());
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
cssParser->loadFromStream(tempCssFile);
|
||||
tempCssFile.close();
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
}
|
||||
|
||||
// Save to cache for next time
|
||||
FsFile cssCacheFile;
|
||||
if (SdMan.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||
cssParser->saveToCache(cssCacheFile);
|
||||
cssCacheFile.close();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files\n", millis(), cssParser->ruleCount(),
|
||||
cssFiles.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
// Initialize spine/TOC cache
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||
cssParser.reset(new CssParser());
|
||||
|
||||
// Try to load existing cache first
|
||||
if (bookMetadataCache->load()) {
|
||||
if (!skipLoadingCss && !loadCssRulesFromCache()) {
|
||||
Serial.printf("[%lu] [EBP] Warning: CSS rules cache not found, attempting to parse CSS files\n", millis());
|
||||
// to get CSS file list
|
||||
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not parse content.opf from cached bookMetadata for CSS files\n", millis());
|
||||
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||
}
|
||||
parseCssFiles();
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -226,6 +306,8 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||
setupCacheDir();
|
||||
|
||||
const uint32_t indexingStart = millis();
|
||||
|
||||
// Begin building cache - stream entries to disk immediately
|
||||
if (!bookMetadataCache->beginWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||
@@ -233,6 +315,7 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
}
|
||||
|
||||
// OPF Pass
|
||||
const uint32_t opfStart = millis();
|
||||
BookMetadataCache::BookMetadata bookMetadata;
|
||||
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
||||
@@ -246,8 +329,10 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
|
||||
|
||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||
const uint32_t tocStart = millis();
|
||||
if (!bookMetadataCache->beginTocPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||
return false;
|
||||
@@ -276,6 +361,7 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
|
||||
|
||||
// Close the cache files
|
||||
if (!bookMetadataCache->endWrite()) {
|
||||
@@ -284,10 +370,13 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
}
|
||||
|
||||
// Build final book.bin
|
||||
const uint32_t buildStart = millis();
|
||||
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
|
||||
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
|
||||
|
||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||
@@ -300,6 +389,11 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!skipLoadingCss) {
|
||||
// Parse CSS files after cache reload
|
||||
parseCssFiles();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -359,7 +453,7 @@ const std::string& Epub::getLanguage() const {
|
||||
}
|
||||
|
||||
std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||
const auto coverFileName = "cover" + cropped ? "_crop" : "";
|
||||
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
|
||||
return cachePath + "/" + coverFileName + ".bmp";
|
||||
}
|
||||
|
||||
@@ -382,7 +476,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
|
||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
FsFile coverJpg;
|
||||
@@ -401,7 +495,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
@@ -419,11 +513,12 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
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
|
||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
||||
if (SdMan.exists(getThumbBmpPath(height).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -435,11 +530,8 @@ bool Epub::generateThumbBmp() const {
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
@@ -455,14 +547,14 @@ bool Epub::generateThumbBmp() const {
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
// 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)
|
||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||
THUMB_TARGET_HEIGHT);
|
||||
coverJpg.close();
|
||||
@@ -471,7 +563,7 @@ bool Epub::generateThumbBmp() const {
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getThumbBmpPath().c_str());
|
||||
SdMan.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
||||
success ? "yes" : "no");
|
||||
@@ -480,6 +572,10 @@ bool Epub::generateThumbBmp() const {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
|
||||
}
|
||||
|
||||
// Write an empty bmp file to avoid generation attempts in the future
|
||||
FsFile thumbBmp;
|
||||
SdMan.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||
thumbBmp.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
#include "Epub/css/CssParser.h"
|
||||
|
||||
class ZipFile;
|
||||
|
||||
@@ -24,11 +25,18 @@ class Epub {
|
||||
std::string cachePath;
|
||||
// Spine and TOC cache
|
||||
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 parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||
bool parseTocNcxFile() const;
|
||||
bool parseTocNavFile() const;
|
||||
void parseCssFiles() const;
|
||||
std::string getCssRulesCache() const;
|
||||
bool loadCssRulesFromCache() const;
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
@@ -37,7 +45,7 @@ class Epub {
|
||||
}
|
||||
~Epub() = default;
|
||||
std::string& getBasePath() { return contentBasePath; }
|
||||
bool load(bool buildIfMissing = true);
|
||||
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||
bool clearCache() const;
|
||||
void setupCacheDir() const;
|
||||
const std::string& getCachePath() const;
|
||||
@@ -48,7 +56,8 @@ class Epub {
|
||||
std::string getCoverBmpPath(bool cropped = false) const;
|
||||
bool generateCoverBmp(bool cropped = false) 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,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
@@ -64,4 +73,5 @@ class Epub {
|
||||
|
||||
size_t getBookSize() const;
|
||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
const CssParser* getCssParser() const { return cssParser.get(); }
|
||||
};
|
||||
|
||||
@@ -40,7 +40,6 @@ bool BookMetadataCache::endContentOpfPass() {
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||
|
||||
// Open spine file for reading
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
@@ -48,12 +47,41 @@ bool BookMetadataCache::beginTocPass() {
|
||||
spineFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
spineHrefIndex.clear();
|
||||
spineHrefIndex.reserve(spineCount);
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
SpineHrefIndexEntry idx;
|
||||
idx.hrefHash = fnvHash64(entry.href);
|
||||
idx.hrefLen = static_cast<uint16_t>(entry.href.size());
|
||||
idx.spineIndex = static_cast<int16_t>(i);
|
||||
spineHrefIndex.push_back(idx);
|
||||
}
|
||||
std::sort(spineHrefIndex.begin(), spineHrefIndex.end(),
|
||||
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||
});
|
||||
spineFile.seek(0);
|
||||
useSpineHrefIndex = true;
|
||||
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
|
||||
} else {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endTocPass() {
|
||||
tocFile.close();
|
||||
spineFile.close();
|
||||
|
||||
spineHrefIndex.clear();
|
||||
spineHrefIndex.shrink_to_fit();
|
||||
useSpineHrefIndex = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -124,6 +152,18 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
// LUTs complete
|
||||
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||
|
||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||
std::vector<int16_t> spineToTocIndex(spineCount, -1);
|
||||
tocFile.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZipFile zip(epubPath);
|
||||
// Pre-open zip file to speed up size calculations
|
||||
if (!zip.open()) {
|
||||
@@ -133,31 +173,56 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
tocFile.close();
|
||||
return false;
|
||||
}
|
||||
// TODO: For large ZIPs loading the all localHeaderOffsets will crash.
|
||||
// However not having them loaded is extremely slow. Need a better solution here.
|
||||
// Perhaps only a cache of spine items or a better way to speedup lookups?
|
||||
if (!zip.loadAllFileStatSlims()) {
|
||||
Serial.printf("[%lu] [BMC] Could not load zip local header offsets for size calculations\n", millis());
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
zip.close();
|
||||
return false;
|
||||
// NOTE: We intentionally skip calling loadAllFileStatSlims() here.
|
||||
// For large EPUBs (2000+ chapters), pre-loading all ZIP central directory entries
|
||||
// into memory causes OOM crashes on ESP32-C3's limited ~380KB RAM.
|
||||
// Instead, for large books we use a one-pass batch lookup that scans the ZIP
|
||||
// central directory once and matches against spine targets using hash comparison.
|
||||
// This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion.
|
||||
// See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134
|
||||
|
||||
std::vector<uint32_t> spineSizes;
|
||||
bool useBatchSizes = false;
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
|
||||
|
||||
std::vector<ZipFile::SizeTarget> targets;
|
||||
targets.reserve(spineCount);
|
||||
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
std::string path = FsHelpers::normalisePath(entry.href);
|
||||
|
||||
ZipFile::SizeTarget t;
|
||||
t.hash = ZipFile::fnvHash64(path.c_str(), path.size());
|
||||
t.len = static_cast<uint16_t>(path.size());
|
||||
t.index = static_cast<uint16_t>(i);
|
||||
targets.push_back(t);
|
||||
}
|
||||
|
||||
std::sort(targets.begin(), targets.end(), [](const ZipFile::SizeTarget& a, const ZipFile::SizeTarget& b) {
|
||||
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||
});
|
||||
|
||||
spineSizes.resize(spineCount, 0);
|
||||
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
|
||||
|
||||
targets.clear();
|
||||
targets.shrink_to_fit();
|
||||
|
||||
useBatchSizes = true;
|
||||
}
|
||||
|
||||
uint32_t cumSize = 0;
|
||||
spineFile.seek(0);
|
||||
int lastSpineTocIndex = -1;
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
|
||||
tocFile.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
if (tocEntry.spineIndex == i) {
|
||||
spineEntry.tocIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
spineEntry.tocIndex = spineToTocIndex[i];
|
||||
|
||||
// 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
|
||||
@@ -169,16 +234,25 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
}
|
||||
lastSpineTocIndex = spineEntry.tocIndex;
|
||||
|
||||
// Calculate size for cumulative size
|
||||
size_t itemSize = 0;
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
if (useBatchSizes) {
|
||||
itemSize = spineSizes[i];
|
||||
if (itemSize == 0) {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
}
|
||||
@@ -248,21 +322,38 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
return;
|
||||
}
|
||||
|
||||
int spineIndex = -1;
|
||||
// find spine index
|
||||
// TODO: This lookup is slow as need to scan through all items each time. We can't hold it all in memory due to size.
|
||||
// But perhaps we can load just the hrefs in a vector/list to do an index lookup?
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
if (spineEntry.href == href) {
|
||||
spineIndex = i;
|
||||
int16_t spineIndex = -1;
|
||||
|
||||
if (useSpineHrefIndex) {
|
||||
uint64_t targetHash = fnvHash64(href);
|
||||
uint16_t targetLen = static_cast<uint16_t>(href.size());
|
||||
|
||||
auto it =
|
||||
std::lower_bound(spineHrefIndex.begin(), spineHrefIndex.end(), SpineHrefIndexEntry{targetHash, targetLen, 0},
|
||||
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||
});
|
||||
|
||||
while (it != spineHrefIndex.end() && it->hrefHash == targetHash && it->hrefLen == targetLen) {
|
||||
spineIndex = it->spineIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
}
|
||||
} else {
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
if (spineEntry.href == href) {
|
||||
spineIndex = static_cast<int16_t>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const TocEntry entry(title, href, anchor, level, spineIndex);
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class BookMetadataCache {
|
||||
public:
|
||||
@@ -53,6 +55,27 @@ class BookMetadataCache {
|
||||
FsFile spineFile;
|
||||
FsFile tocFile;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
uint64_t hrefHash; // FNV-1a 64-bit hash
|
||||
uint16_t hrefLen; // length for collision reduction
|
||||
int16_t spineIndex;
|
||||
};
|
||||
std::vector<SpineHrefIndexEntry> spineHrefIndex;
|
||||
bool useSpineHrefIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a 64-bit hash function
|
||||
static uint64_t fnvHash64(const std::string& s) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (char c : s) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint32_t writeSpineEntry(FsFile& file, const SpineEntry& entry) const;
|
||||
uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const;
|
||||
SpineEntry readSpineEntry(FsFile& file) const;
|
||||
|
||||
+143
-42
@@ -49,11 +49,17 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
||||
|
||||
} // 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;
|
||||
|
||||
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
|
||||
@@ -70,17 +76,21 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
const int pageWidth = viewportWidth;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
// Build indexed continues vector from the parallel list for O(1) access during layout
|
||||
std::vector<bool> continuesVec(wordContinues.begin(), wordContinues.end());
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
if (hyphenationEnabled) {
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||
} 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;
|
||||
|
||||
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 +114,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,
|
||||
const int spaceWidth, std::vector<uint16_t>& wordWidths) {
|
||||
const int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
if (words.empty()) {
|
||||
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.
|
||||
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
||||
while (wordWidths[i] > pageWidth) {
|
||||
if (!hyphenateWordAtIndex(i, pageWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
|
||||
// First word needs to fit in reduced width if there's an indent
|
||||
const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
while (wordWidths[i] > effectiveWidth) {
|
||||
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true,
|
||||
&continuesVec)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -130,22 +151,31 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
ans[totalWordCount - 1] = totalWordCount - 1;
|
||||
|
||||
for (int i = totalWordCount - 2; i >= 0; --i) {
|
||||
int currlen = -spaceWidth;
|
||||
int currlen = 0;
|
||||
dp[i] = MAX_COST;
|
||||
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// Current line length: previous width + space + current word width
|
||||
currlen += wordWidths[j] + spaceWidth;
|
||||
// First line has reduced width due to text-indent
|
||||
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Cannot break after word j if the next word attaches to it (continuation group)
|
||||
if (j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int cost;
|
||||
if (j == totalWordCount - 1) {
|
||||
cost = 0; // Last line
|
||||
} else {
|
||||
const int remainingSpace = pageWidth - currlen;
|
||||
const int remainingSpace = effectivePageWidth - currlen;
|
||||
// Use long long for the square to prevent overflow
|
||||
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
|
||||
|
||||
@@ -200,7 +230,11 @@ void ParsedText::applyParagraphIndent() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -208,33 +242,45 @@ void ParsedText::applyParagraphIndent() {
|
||||
// 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,
|
||||
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;
|
||||
size_t currentIndex = 0;
|
||||
bool isFirstLine = true;
|
||||
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
const size_t lineStart = currentIndex;
|
||||
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
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
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];
|
||||
|
||||
// Word fits on current line
|
||||
if (lineWidth + candidateWidth <= pageWidth) {
|
||||
if (lineWidth + candidateWidth <= effectivePageWidth) {
|
||||
lineWidth += candidateWidth;
|
||||
++currentIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if (availableWidth > 0 &&
|
||||
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
||||
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
||||
allowFallbackBreaks, &continuesVec)) {
|
||||
// Prefix now fits; append it to this line and move to next line
|
||||
lineWidth += spacing + wordWidths[currentIndex];
|
||||
++currentIndex;
|
||||
@@ -249,7 +295,14 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
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);
|
||||
isFirstLine = false;
|
||||
}
|
||||
|
||||
return lineBreakIndices;
|
||||
@@ -259,7 +312,7 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
// available width.
|
||||
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
||||
const int fontId, std::vector<uint16_t>& wordWidths,
|
||||
const bool allowFallbackBreaks) {
|
||||
const bool allowFallbackBreaks, std::vector<bool>* continuesVec) {
|
||||
// Guard against invalid indices or zero available width before attempting to split.
|
||||
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
||||
return false;
|
||||
@@ -314,12 +367,28 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
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 insertStyleIt = std::next(styleIt);
|
||||
words.insert(insertWordIt, remainder);
|
||||
wordStyles.insert(insertStyleIt, style);
|
||||
|
||||
// The remainder inherits whatever continuation status the original word had with the word after it.
|
||||
// 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.
|
||||
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
||||
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
|
||||
@@ -328,49 +397,76 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
}
|
||||
|
||||
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 size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||
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;
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
lineWordWidthSum += wordWidths[i];
|
||||
size_t actualGapCount = 0;
|
||||
|
||||
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
|
||||
const int spareSpace = pageWidth - lineWordWidthSum;
|
||||
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||
const int effectivePageWidth = pageWidth - firstLineIndent;
|
||||
const int spareSpace = effectivePageWidth - lineWordWidthSum;
|
||||
|
||||
int spacing = spaceWidth;
|
||||
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
||||
|
||||
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
// For justified text, calculate spacing based on actual gap count
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1) {
|
||||
spacing = spareSpace / static_cast<int>(actualGapCount);
|
||||
}
|
||||
|
||||
// Calculate initial x position
|
||||
uint16_t xpos = 0;
|
||||
if (style == TextBlock::RIGHT_ALIGN) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
// Calculate initial x position (first line starts at indent for left/justified text)
|
||||
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
||||
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
||||
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||
xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
// Continuation words attach to the previous word with no space before them
|
||||
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);
|
||||
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
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
auto wordContinuesEndIt = wordContinues.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
std::advance(wordContinuesEndIt, lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
@@ -378,11 +474,16 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
|
||||
// Consume continues flags (not passed to TextBlock, but must be consumed to stay in sync)
|
||||
std::list<bool> lineContinues;
|
||||
lineContinues.splice(lineContinues.begin(), wordContinues, wordContinues.begin(), wordContinuesEndIt);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
if (containsSoftHyphen(word)) {
|
||||
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));
|
||||
}
|
||||
|
||||
+15
-11
@@ -8,6 +8,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/BlockStyle.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
class GfxRenderer;
|
||||
@@ -15,31 +16,34 @@ class GfxRenderer;
|
||||
class ParsedText {
|
||||
std::list<std::string> words;
|
||||
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 hyphenationEnabled;
|
||||
|
||||
void applyParagraphIndent();
|
||||
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,
|
||||
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,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||
std::vector<bool>* continuesVec = nullptr);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
public:
|
||||
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
|
||||
const bool hyphenationEnabled = false)
|
||||
: style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||
const BlockStyle& blockStyle = BlockStyle())
|
||||
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle);
|
||||
void setStyle(const TextBlock::Style style) { this->style = style; }
|
||||
TextBlock::Style getStyle() const { return style; }
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||
size_t size() const { return words.size(); }
|
||||
bool isEmpty() const { return words.empty(); }
|
||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||
|
||||
+14
-17
@@ -8,9 +8,9 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 10;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
||||
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);
|
||||
} // namespace
|
||||
|
||||
@@ -33,7 +33,8 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
|
||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool embeddedStyle) {
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
||||
return;
|
||||
@@ -41,7 +42,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(uint32_t),
|
||||
sizeof(embeddedStyle) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -51,13 +52,14 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
serialization::writePod(file, viewportWidth);
|
||||
serialization::writePod(file, viewportHeight);
|
||||
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, static_cast<uint32_t>(0)); // Placeholder for LUT offset
|
||||
}
|
||||
|
||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -79,6 +81,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
bool fileExtraParagraphSpacing;
|
||||
uint8_t fileParagraphAlignment;
|
||||
bool fileHyphenationEnabled;
|
||||
bool fileEmbeddedStyle;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
@@ -86,11 +89,12 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, fileViewportWidth);
|
||||
serialization::readPod(file, fileViewportHeight);
|
||||
serialization::readPod(file, fileHyphenationEnabled);
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled) {
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||
clearCache();
|
||||
@@ -122,10 +126,8 @@ bool Section::clearCache() const {
|
||||
|
||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const std::function<void()>& progressSetupFn,
|
||||
const std::function<void(int)>& progressFn) {
|
||||
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const std::function<void()>& popupFn) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||
|
||||
@@ -171,23 +173,18 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
|
||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
||||
|
||||
// Only show progress bar for larger chapters where rendering overhead is worth it
|
||||
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
||||
progressSetupFn();
|
||||
}
|
||||
|
||||
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled);
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle);
|
||||
std::vector<uint32_t> lut = {};
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||
progressFn);
|
||||
embeddedStyle, popupFn, embeddedStyle ? epub->getCssParser() : nullptr);
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ class Section {
|
||||
FsFile file;
|
||||
|
||||
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);
|
||||
|
||||
public:
|
||||
@@ -29,11 +30,10 @@ class Section {
|
||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||
~Section() = default;
|
||||
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 createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
const std::function<void()>& progressSetupFn = nullptr,
|
||||
const std::function<void(int)>& progressFn = nullptr);
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
const std::function<void()>& popupFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -14,9 +14,32 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
|
||||
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, std::string("\xe2\x80\x83").c_str());
|
||||
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(wordStylesIt, 1);
|
||||
@@ -37,8 +60,19 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||
|
||||
// Block style
|
||||
serialization::writePod(file, style);
|
||||
// Style (alignment + margins/padding/indent)
|
||||
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;
|
||||
}
|
||||
@@ -48,7 +82,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
serialization::readPod(file, wc);
|
||||
@@ -67,8 +101,20 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
|
||||
// Block style
|
||||
serialization::readPod(file, style);
|
||||
// Style (alignment + margins/padding/indent)
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -7,30 +7,26 @@
|
||||
#include <string>
|
||||
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
|
||||
// Represents a line of text on a page
|
||||
class TextBlock final : public Block {
|
||||
public:
|
||||
enum Style : uint8_t {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
RIGHT_ALIGN = 3,
|
||||
};
|
||||
|
||||
private:
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||
std::list<EpdFontFamily::Style> word_styles, const Style style)
|
||||
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), 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)),
|
||||
blockStyle(blockStyle) {}
|
||||
~TextBlock() override = default;
|
||||
void setStyle(const Style style) { this->style = style; }
|
||||
Style getStyle() const { return style; }
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
void layout(GfxRenderer& renderer) override {};
|
||||
// given a renderer works out where to break the words into lines
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
#include "CssParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace {
|
||||
|
||||
// Buffer size for reading CSS files
|
||||
constexpr size_t READ_BUFFER_SIZE = 512;
|
||||
|
||||
// Maximum CSS file size we'll process (prevent memory issues)
|
||||
constexpr size_t MAX_CSS_SIZE = 64 * 1024;
|
||||
|
||||
// Check if character is CSS whitespace
|
||||
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||
|
||||
// Read entire file into string (with size limit)
|
||||
std::string readFileContent(FsFile& file) {
|
||||
std::string content;
|
||||
content.reserve(std::min(static_cast<size_t>(file.size()), MAX_CSS_SIZE));
|
||||
|
||||
char buffer[READ_BUFFER_SIZE];
|
||||
while (file.available() && content.size() < MAX_CSS_SIZE) {
|
||||
const int bytesRead = file.read(buffer, sizeof(buffer));
|
||||
if (bytesRead <= 0) break;
|
||||
content.append(buffer, bytesRead);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Remove CSS comments (/* ... */) from content
|
||||
std::string stripComments(const std::string& css) {
|
||||
std::string result;
|
||||
result.reserve(css.size());
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < css.size()) {
|
||||
// Look for start of comment
|
||||
if (pos + 1 < css.size() && css[pos] == '/' && css[pos + 1] == '*') {
|
||||
// Find end of comment
|
||||
const size_t endPos = css.find("*/", pos + 2);
|
||||
if (endPos == std::string::npos) {
|
||||
// Unterminated comment - skip rest of file
|
||||
break;
|
||||
}
|
||||
pos = endPos + 2;
|
||||
} else {
|
||||
result.push_back(css[pos]);
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Skip @-rules (like @media, @import, @font-face)
|
||||
// Returns position after the @-rule
|
||||
size_t skipAtRule(const std::string& css, const size_t start) {
|
||||
// Find the end - either semicolon (simple @-rule) or matching brace
|
||||
size_t pos = start + 1; // Skip the '@'
|
||||
|
||||
// Skip identifier
|
||||
while (pos < css.size() && (std::isalnum(css[pos]) || css[pos] == '-')) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Look for { or ;
|
||||
int braceDepth = 0;
|
||||
while (pos < css.size()) {
|
||||
const char c = css[pos];
|
||||
if (c == '{') {
|
||||
++braceDepth;
|
||||
} else if (c == '}') {
|
||||
--braceDepth;
|
||||
if (braceDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
} else if (c == ';' && braceDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
return css.size();
|
||||
}
|
||||
|
||||
// Extract next rule from CSS content
|
||||
// Returns true if a rule was found, with selector and body filled
|
||||
bool extractNextRule(const std::string& css, size_t& pos, std::string& selector, std::string& body) {
|
||||
selector.clear();
|
||||
body.clear();
|
||||
|
||||
// Skip whitespace and @-rules until we find a regular rule
|
||||
while (pos < css.size()) {
|
||||
// Skip whitespace
|
||||
while (pos < css.size() && isCssWhitespace(css[pos])) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
if (pos >= css.size()) return false;
|
||||
|
||||
// Handle @-rules iteratively (avoids recursion/stack overflow)
|
||||
if (css[pos] == '@') {
|
||||
pos = skipAtRule(css, pos);
|
||||
continue; // Try again after skipping the @-rule
|
||||
}
|
||||
|
||||
break; // Found start of a regular rule
|
||||
}
|
||||
|
||||
if (pos >= css.size()) return false;
|
||||
|
||||
// Find opening brace
|
||||
const size_t bracePos = css.find('{', pos);
|
||||
if (bracePos == std::string::npos) return false;
|
||||
|
||||
// Extract selector (everything before the brace)
|
||||
selector = css.substr(pos, bracePos - pos);
|
||||
|
||||
// Find matching closing brace
|
||||
int depth = 1;
|
||||
const size_t bodyStart = bracePos + 1;
|
||||
size_t bodyEnd = bodyStart;
|
||||
|
||||
while (bodyEnd < css.size() && depth > 0) {
|
||||
if (css[bodyEnd] == '{')
|
||||
++depth;
|
||||
else if (css[bodyEnd] == '}')
|
||||
--depth;
|
||||
++bodyEnd;
|
||||
}
|
||||
|
||||
// Extract body (between braces)
|
||||
if (bodyEnd > bodyStart) {
|
||||
body = css.substr(bodyStart, bodyEnd - bodyStart - 1);
|
||||
}
|
||||
|
||||
pos = bodyEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// String utilities implementation
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
int8_t CssParser::interpretSpacing(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
if (v.empty()) return 0;
|
||||
|
||||
// For spacing, we convert to "lines" (discrete units for e-ink)
|
||||
// 1em ≈ 1 line, percentages based on ~30 lines per page
|
||||
|
||||
float multiplier = 0.0f;
|
||||
size_t unitStart = v.size();
|
||||
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
const char c = v[i];
|
||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||
unitStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string numPart = v.substr(0, unitStart);
|
||||
const std::string unitPart = v.substr(unitStart);
|
||||
|
||||
if (unitPart == "em" || unitPart == "rem") {
|
||||
multiplier = 1.0f; // 1em = 1 line
|
||||
} else if (unitPart == "%") {
|
||||
multiplier = 0.3f; // ~30 lines per page, so 10% = 3 lines
|
||||
} else {
|
||||
return 0; // Unsupported unit for spacing
|
||||
}
|
||||
|
||||
char* endPtr = nullptr;
|
||||
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
||||
|
||||
if (endPtr == numPart.c_str()) return 0;
|
||||
|
||||
int lines = static_cast<int>(numericValue * multiplier);
|
||||
|
||||
// Clamp to reasonable range (0-2 lines)
|
||||
if (lines < 0) lines = 0;
|
||||
if (lines > 2) lines = 2;
|
||||
|
||||
return static_cast<int8_t>(lines);
|
||||
}
|
||||
|
||||
// Declaration parsing
|
||||
|
||||
CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
CssStyle style;
|
||||
|
||||
// Split declarations by semicolon
|
||||
const auto declarations = splitOnChar(declBlock, ';');
|
||||
|
||||
for (const auto& decl : declarations) {
|
||||
// Find colon separator
|
||||
const size_t colonPos = decl.find(':');
|
||||
if (colonPos == std::string::npos || colonPos == 0) continue;
|
||||
|
||||
std::string propName = normalized(decl.substr(0, colonPos));
|
||||
std::string propValue = normalized(decl.substr(colonPos + 1));
|
||||
|
||||
if (propName.empty() || propValue.empty()) continue;
|
||||
|
||||
// Match property and set value
|
||||
if (propName == "text-align") {
|
||||
style.textAlign = interpretAlignment(propValue);
|
||||
style.defined.textAlign = 1;
|
||||
} else if (propName == "font-style") {
|
||||
style.fontStyle = interpretFontStyle(propValue);
|
||||
style.defined.fontStyle = 1;
|
||||
} else if (propName == "font-weight") {
|
||||
style.fontWeight = interpretFontWeight(propValue);
|
||||
style.defined.fontWeight = 1;
|
||||
} else if (propName == "text-decoration" || propName == "text-decoration-line") {
|
||||
style.textDecoration = interpretDecoration(propValue);
|
||||
style.defined.textDecoration = 1;
|
||||
} else if (propName == "text-indent") {
|
||||
style.textIndent = interpretLength(propValue);
|
||||
style.defined.textIndent = 1;
|
||||
} else if (propName == "margin-top") {
|
||||
style.marginTop = interpretLength(propValue);
|
||||
style.defined.marginTop = 1;
|
||||
} else if (propName == "margin-bottom") {
|
||||
style.marginBottom = interpretLength(propValue);
|
||||
style.defined.marginBottom = 1;
|
||||
} else if (propName == "margin-left") {
|
||||
style.marginLeft = interpretLength(propValue);
|
||||
style.defined.marginLeft = 1;
|
||||
} else if (propName == "margin-right") {
|
||||
style.marginRight = interpretLength(propValue);
|
||||
style.defined.marginRight = 1;
|
||||
} else if (propName == "margin") {
|
||||
// Shorthand: 1-4 values for top, right, bottom, left
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (!values.empty()) {
|
||||
style.marginTop = interpretLength(values[0]);
|
||||
style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop;
|
||||
style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop;
|
||||
style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight;
|
||||
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (propName == "padding-top") {
|
||||
style.paddingTop = interpretLength(propValue);
|
||||
style.defined.paddingTop = 1;
|
||||
} else if (propName == "padding-bottom") {
|
||||
style.paddingBottom = interpretLength(propValue);
|
||||
style.defined.paddingBottom = 1;
|
||||
} else if (propName == "padding-left") {
|
||||
style.paddingLeft = interpretLength(propValue);
|
||||
style.defined.paddingLeft = 1;
|
||||
} else if (propName == "padding-right") {
|
||||
style.paddingRight = interpretLength(propValue);
|
||||
style.defined.paddingRight = 1;
|
||||
} else if (propName == "padding") {
|
||||
// Shorthand: 1-4 values for top, right, bottom, left
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (!values.empty()) {
|
||||
style.paddingTop = interpretLength(values[0]);
|
||||
style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop;
|
||||
style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop;
|
||||
style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight;
|
||||
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom =
|
||||
style.defined.paddingLeft = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
// Rule processing
|
||||
|
||||
void CssParser::processRuleBlock(const std::string& selectorGroup, const std::string& declarations) {
|
||||
const CssStyle style = parseDeclarations(declarations);
|
||||
|
||||
// Only store if any properties were set
|
||||
if (!style.defined.anySet()) return;
|
||||
|
||||
// Handle comma-separated selectors
|
||||
const auto selectors = splitOnChar(selectorGroup, ',');
|
||||
|
||||
for (const auto& sel : selectors) {
|
||||
// Normalize the selector
|
||||
std::string key = normalized(sel);
|
||||
if (key.empty()) continue;
|
||||
|
||||
// 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) {
|
||||
Serial.printf("[%lu] [CSS] Cannot read from invalid file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const std::string content = readFileContent(source);
|
||||
if (content.empty()) {
|
||||
return true; // Empty file is valid
|
||||
}
|
||||
|
||||
// Remove comments
|
||||
const std::string cleaned = stripComments(content);
|
||||
|
||||
// Parse rules
|
||||
size_t pos = 0;
|
||||
std::string selector, body;
|
||||
|
||||
while (extractNextRule(cleaned, pos, selector, body)) {
|
||||
processRuleBlock(selector, body);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Parsed %zu rules\n", millis(), rulesBySelector_.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Style resolution
|
||||
|
||||
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
|
||||
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;
|
||||
|
||||
bool CssParser::saveToCache(FsFile& file) const {
|
||||
if (!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));
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Saved %u rules to cache\n", millis(), ruleCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CssParser::loadFromCache(FsFile& file) {
|
||||
if (!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) {
|
||||
Serial.printf("[%lu] [CSS] Cache version mismatch (got %u, expected %u)\n", millis(), version, CSS_CACHE_VERSION);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read rule count
|
||||
uint16_t ruleCount = 0;
|
||||
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string selector;
|
||||
selector.resize(selectorLen);
|
||||
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read CssStyle fields
|
||||
CssStyle style;
|
||||
uint8_t enumVal;
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.textAlign = static_cast<CssTextAlign>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.fontStyle = static_cast<CssFontStyle>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.fontWeight = static_cast<CssFontWeight>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read defined flags
|
||||
uint16_t definedBits = 0;
|
||||
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
|
||||
rulesBySelector_.clear();
|
||||
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;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Loaded %u rules from cache\n", millis(), ruleCount);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#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:
|
||||
CssParser() = default;
|
||||
~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(); }
|
||||
|
||||
/**
|
||||
* Save parsed CSS rules to a cache file.
|
||||
* @param file Open file handle to write to
|
||||
* @return true if cache was written successfully
|
||||
*/
|
||||
bool saveToCache(FsFile& file) const;
|
||||
|
||||
/**
|
||||
* Load CSS rules from a cache file.
|
||||
* Clears any existing rules before loading.
|
||||
* @param file Open file handle to read from
|
||||
* @return true if cache was loaded successfully
|
||||
*/
|
||||
bool loadFromCache(FsFile& file);
|
||||
|
||||
private:
|
||||
// Storage: maps normalized selector -> style properties
|
||||
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
||||
|
||||
// Internal parsing helpers
|
||||
void processRuleBlock(const std::string& selectorGroup, const std::string& declarations);
|
||||
static CssStyle parseDeclarations(const std::string& declBlock);
|
||||
|
||||
// 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);
|
||||
static int8_t interpretSpacing(const std::string& val);
|
||||
|
||||
// String utilities
|
||||
static std::string normalized(const std::string& s);
|
||||
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
|
||||
static std::vector<std::string> splitWhitespace(const std::string& s);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -125,6 +125,8 @@ bool isExplicitHyphen(const uint32_t cp) {
|
||||
case 0xFE58: // small em dash
|
||||
case 0xFE63: // small hyphen-minus
|
||||
case 0xFF0D: // fullwidth hyphen-minus
|
||||
case 0x005F: // Underscore
|
||||
case 0x2026: // Ellipsis
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "HyphenationCommon.h"
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
|
||||
@@ -16,14 +17,16 @@ LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 4>;
|
||||
using EntryArray = std::array<LanguageEntry, 5>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
{"german", "de", &germanHyphenator},
|
||||
{"russian", "ru", &russianHyphenator}}};
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
{"spanish", "es", &spanishHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
#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 es_trie_data[] = {
|
||||
0x00, 0x00, 0x34, 0xFC, 0x01, 0x04, 0x16, 0x02, 0x0E, 0x0C, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x22, 0x0F, 0x2C, 0x0F,
|
||||
0x22, 0x0D, 0x2C, 0x0D, 0x0B, 0x16, 0x0B, 0x20, 0x15, 0x16, 0x15, 0x0C, 0x02, 0x0C, 0x17, 0x0E, 0x04, 0x2C, 0x05,
|
||||
0x04, 0x0D, 0x04, 0x21, 0x04, 0x18, 0x0D, 0x04, 0x17, 0x04, 0x0D, 0x17, 0x04, 0x0E, 0x0D, 0x04, 0x0D, 0x21, 0x04,
|
||||
0x0D, 0x21, 0x21, 0x0F, 0x0E, 0x0F, 0x0E, 0x0D, 0x0F, 0x0E, 0x17, 0x33, 0x33, 0x0C, 0x33, 0x16, 0x29, 0x29, 0x0C,
|
||||
0x29, 0x16, 0x21, 0x0C, 0x21, 0x0E, 0x34, 0x0D, 0x3E, 0x36, 0x0D, 0x3F, 0x2B, 0x16, 0x0D, 0x3D, 0x3D, 0x0C, 0x3D,
|
||||
0x16, 0x1F, 0x1F, 0x16, 0x2A, 0x2C, 0x0D, 0x0E, 0x0E, 0x21, 0x1F, 0x0C, 0x2A, 0x0D, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C,
|
||||
0x2A, 0x0B, 0x16, 0x37, 0x20, 0x0C, 0x20, 0x16, 0x35, 0x24, 0x47, 0x47, 0x0C, 0x47, 0x16, 0x20, 0x0B, 0x20, 0x0D,
|
||||
0x0C, 0x20, 0x0D, 0x16, 0x20, 0x20, 0x03, 0x17, 0x0E, 0x0D, 0x23, 0x0E, 0x17, 0x17, 0x17, 0x21, 0x16, 0x0D, 0x18,
|
||||
0x48, 0x49, 0x16, 0x0C, 0x0C, 0x16, 0x0C, 0x16, 0x2D, 0x2B, 0x0E, 0x0D, 0x2B, 0x0E, 0x17, 0x17, 0x2B, 0x34, 0x0B,
|
||||
0x34, 0x0B, 0x0C, 0x34, 0x0B, 0x16, 0x21, 0x20, 0x0D, 0x21, 0x0E, 0x17, 0x20, 0x0D, 0x04, 0x0F, 0x19, 0x0C, 0x0D,
|
||||
0x2E, 0x0F, 0x0E, 0x21, 0x17, 0x0E, 0x2D, 0x0E, 0x2B, 0x0E, 0x22, 0x17, 0x17, 0x0E, 0x22, 0x0D, 0x0E, 0x38, 0x19,
|
||||
0x18, 0x03, 0x0C, 0x22, 0x0B, 0x0E, 0x22, 0x0B, 0x18, 0x40, 0x2A, 0x0C, 0x0C, 0x2A, 0x0C, 0x16, 0x18, 0x0D, 0x0C,
|
||||
0x18, 0x0D, 0x0E, 0x2B, 0x21, 0x2B, 0x17, 0x2A, 0x16, 0x02, 0x33, 0x02, 0x33, 0x0C, 0x02, 0x33, 0x16, 0x35, 0x0E,
|
||||
0x04, 0x0C, 0x20, 0x0C, 0x0C, 0x20, 0x0C, 0x16, 0x2B, 0x0E, 0x0E, 0x2B, 0x0E, 0x18, 0x04, 0x0D, 0x0E, 0x0D, 0x19,
|
||||
0x0E, 0x41, 0x10, 0x2A, 0x20, 0x04, 0x0C, 0x0D, 0x03, 0x0E, 0x16, 0x0D, 0x0E, 0x18, 0x0F, 0x05, 0x0E, 0x07, 0x0E,
|
||||
0xA0, 0x00, 0x51, 0xA0, 0x00, 0x71, 0xA0, 0x00, 0xC3, 0xA3, 0x00, 0x71, 0x74, 0x6E, 0x7A, 0xFD, 0xFD, 0xFD, 0xA1,
|
||||
0x00, 0x71, 0x74, 0xF4, 0xA1, 0x00, 0x71, 0x6E, 0xEF, 0xA3, 0x00, 0x71, 0x74, 0x73, 0x6E, 0xEA, 0xEA, 0xEA, 0xA2,
|
||||
0x00, 0x71, 0x7A, 0x73, 0xE1, 0xE1, 0xA0, 0x00, 0xA2, 0xB6, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xD1, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
||||
0x01, 0xD2, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x05, 0xB1, 0xA0, 0x05, 0xC2, 0xA0, 0x05,
|
||||
0xE2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x05,
|
||||
0x81, 0xA0, 0x06, 0x72, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x05, 0x81, 0x6F, 0x61, 0xFA, 0xFD, 0xAE, 0x06,
|
||||
0x31, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x70, 0x71, 0x73, 0x74, 0x76, 0x7A, 0xED, 0xED, 0xF9, 0xED,
|
||||
0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0x21, 0x6E, 0xE1, 0xA0, 0x06, 0x01, 0xA0, 0x06, 0x92,
|
||||
0xA0, 0x06, 0x12, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x51, 0x21, 0x61,
|
||||
0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6F, 0xFD, 0x28, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0x6C, 0xDA, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xE3, 0xFD, 0x44, 0x75, 0x62, 0x65, 0x6F, 0xFF, 0x65, 0xFF,
|
||||
0x91, 0xFF, 0xC6, 0xFF, 0xEF, 0xA0, 0x04, 0x41, 0xA0, 0x04, 0x52, 0xA0, 0x04, 0x72, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xF5, 0x21, 0x61, 0xF1, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x69, 0x75,
|
||||
0xFE, 0xC5, 0xFE, 0xC8, 0xFE, 0xCE, 0xFE, 0xC8, 0xFE, 0xD7, 0xFE, 0xDC, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE,
|
||||
0xDC, 0xFE, 0xC8, 0xFE, 0xE1, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xEA, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8,
|
||||
0xFE, 0xC8, 0xFE, 0xF4, 0xFE, 0xF4, 0xFF, 0xC7, 0xFF, 0xFD, 0x41, 0x6C, 0xFF, 0x45, 0x21, 0x61, 0xFC, 0x21, 0x75,
|
||||
0xFD, 0x41, 0x72, 0xFF, 0x3B, 0x22, 0x6E, 0x75, 0xF9, 0xFC, 0x41, 0x78, 0xFF, 0x32, 0x41, 0x78, 0xFF, 0x34, 0x21,
|
||||
0xB3, 0xFC, 0x41, 0x6E, 0xFF, 0x27, 0xA0, 0x01, 0x52, 0x21, 0x64, 0xFD, 0xA0, 0x06, 0x43, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x65, 0xFA, 0x23, 0x6E, 0x70, 0x76, 0xF4, 0xFA, 0xFD, 0x21, 0x74, 0xEA, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFA, 0x21,
|
||||
0x69, 0xED, 0x21, 0x6C, 0xFD, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xEA, 0xF4, 0xF7, 0xFD, 0x21, 0x6E, 0xF7, 0x25, 0x61,
|
||||
0x6F, 0xC3, 0x75, 0x65, 0xBB, 0xC0, 0xC8, 0xCB, 0xFD, 0xA1, 0x00, 0x61, 0x69, 0xF5, 0xA0, 0x07, 0xB1, 0x21, 0x62,
|
||||
0xFD, 0xA0, 0x00, 0xF1, 0x21, 0x68, 0xFD, 0x22, 0x69, 0x6F, 0xFA, 0xFA, 0x21, 0x74, 0xF5, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x24, 0x63, 0x73, 0x72, 0x74, 0xEF, 0xF2, 0xFD, 0xEC, 0xA2, 0x06, 0x01, 0x69, 0x65, 0xE0, 0xF7, 0xA0,
|
||||
0x02, 0x91, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFA, 0x21, 0x65, 0xFD, 0x22, 0x65, 0x72, 0xF7, 0xFD, 0x21, 0x6E, 0xEF,
|
||||
0x41, 0x6C, 0xFE, 0x6F, 0x22, 0x65, 0x75, 0xF9, 0xFC, 0x21, 0x74, 0xE3, 0x21, 0x73, 0xFD, 0x21, 0xB3, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x41, 0x63, 0xFE, 0x5A, 0x21, 0x73, 0xFC, 0x22, 0x65, 0x69, 0xFD, 0xF9, 0x21, 0x64, 0xCB, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xD3, 0x21, 0x69, 0xFD, 0x21, 0x6F, 0xB9, 0x21, 0x74, 0xFD,
|
||||
0xA7, 0x07, 0x62, 0x63, 0x67, 0x70, 0x6C, 0x72, 0x78, 0x75, 0xBF, 0xCB, 0xD9, 0xE3, 0xF1, 0xF7, 0xFD, 0x42, 0x63,
|
||||
0x74, 0xFF, 0xA2, 0xFF, 0xA2, 0x41, 0x63, 0xFF, 0x9B, 0x22, 0x69, 0x75, 0xF5, 0xFC, 0x41, 0x69, 0xFF, 0x92, 0x21,
|
||||
0x63, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0xA1, 0xFE, 0x0B, 0x21, 0xC3, 0xFC, 0x41, 0x73, 0xFF, 0x81, 0x21, 0x69, 0xFC,
|
||||
0xA4, 0x07, 0x62, 0x64, 0x66, 0x74, 0x78, 0xE3, 0xEF, 0xF6, 0xFD, 0x41, 0x75, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41,
|
||||
0x6F, 0xFD, 0xEB, 0xA2, 0x07, 0x62, 0x6D, 0x74, 0xF9, 0xFC, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32,
|
||||
0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x43, 0x65, 0xC3, 0x74, 0xFF, 0xFA, 0xFF, 0xFD, 0xFF, 0x2A, 0x41, 0x6E, 0xFF,
|
||||
0x20, 0x21, 0xAD, 0xFC, 0x23, 0x65, 0x69, 0xC3, 0xF9, 0xF9, 0xFD, 0x21, 0x64, 0xF9, 0xA3, 0x05, 0x02, 0x6B, 0x70,
|
||||
0x72, 0xD9, 0xE5, 0xFD, 0xA0, 0x07, 0x62, 0xA0, 0x07, 0xA1, 0x21, 0x6C, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x07, 0x82,
|
||||
0x67, 0xFD, 0xA0, 0x07, 0x82, 0xC1, 0x07, 0x82, 0x70, 0xFE, 0xFD, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF2, 0xF7,
|
||||
0xF7, 0xFA, 0xF7, 0xA0, 0x01, 0xA1, 0x21, 0x62, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x48, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0x6E, 0xFE, 0xF2, 0xFF, 0x46, 0xFF, 0x7F, 0xFF, 0x95, 0xFF, 0xC6, 0xFF, 0xCF, 0xFF, 0xE9,
|
||||
0xFF, 0xFD, 0xA0, 0x0A, 0x01, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0xA2, 0x00, 0x61, 0x6F, 0x61, 0xDE, 0xFD, 0xA0,
|
||||
0x08, 0x12, 0xA0, 0x08, 0x33, 0xC2, 0x07, 0x82, 0x6D, 0x6E, 0xFD, 0x4D, 0xFD, 0x4D, 0xA0, 0x0B, 0x45, 0x23, 0xA1,
|
||||
0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6, 0xF9, 0x21, 0x73, 0xF7, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA1, 0x07, 0x82, 0x6E, 0xFD, 0xA0, 0x08, 0x63, 0xA0,
|
||||
0x08, 0x92, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFA, 0xFD, 0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xFF, 0xB9, 0xFF, 0xBC, 0xFF, 0xBF, 0xFF, 0xEA, 0xFF, 0x70, 0xFF, 0x70, 0xFF, 0xF5, 0x42, 0x73, 0x75,
|
||||
0xFF, 0xEA, 0xFF, 0x96, 0xA0, 0x09, 0x91, 0x21, 0x68, 0xFD, 0xA1, 0x09, 0x81, 0x63, 0xFD, 0x21, 0x6F, 0xFB, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0x61, 0x65, 0x69, 0xE2, 0xFD, 0xA0, 0x00, 0x61, 0xA0,
|
||||
0x0C, 0xC3, 0x21, 0x75, 0xFD, 0xA0, 0x04, 0x91, 0x21, 0x74, 0xFD, 0x22, 0x64, 0x73, 0xF7, 0xFD, 0x22, 0x6E, 0x73,
|
||||
0xF5, 0xF5, 0x21, 0x6F, 0xFB, 0x22, 0x74, 0x7A, 0xED, 0xED, 0x21, 0x6E, 0xFB, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0x43, 0x63, 0x65, 0x6E, 0xFF, 0xEF, 0xFD, 0x20, 0xFF, 0xFD, 0x21, 0x6E, 0xD8, 0x23, 0x65, 0x61, 0x69, 0xD8, 0xF3,
|
||||
0xFD, 0x21, 0x6C, 0xF9, 0x41, 0x69, 0xFD, 0x1D, 0xA0, 0x04, 0xA2, 0xA0, 0x04, 0xC2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xB3, 0xEF, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xF5, 0x22, 0x6C, 0x6F, 0xDC, 0xF1, 0xA2, 0x00, 0x61, 0x61, 0x69, 0xD4, 0xFB, 0xA0, 0x0D, 0x43, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62, 0xF4, 0x21, 0xA1, 0xFD, 0x22, 0xC3, 0x61, 0xFD, 0xFA, 0x23,
|
||||
0x66, 0x6D, 0x72, 0xEF, 0xF2, 0xFB, 0xA0, 0x0D, 0x73, 0x21, 0x62, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA0,
|
||||
0x0D, 0x42, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x70, 0xFD, 0x22, 0xA1, 0xB3, 0xF1, 0xFD, 0x21, 0x70, 0xEF,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x6D, 0xE3, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xFA,
|
||||
0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFE, 0x28, 0x21, 0x73, 0xFC, 0x21, 0x6C, 0xCB, 0x22, 0x69,
|
||||
0x65, 0xFA, 0xFD, 0x45, 0x61, 0xC3, 0x65, 0x69, 0x68, 0xFF, 0xB0, 0xFF, 0xCF, 0xFF, 0xDD, 0xFF, 0xEE, 0xFF, 0xFB,
|
||||
0x21, 0x6E, 0xF0, 0xA0, 0x06, 0xD2, 0x41, 0x69, 0xFB, 0xE3, 0xA0, 0x0E, 0x92, 0x21, 0x65, 0xFD, 0xC3, 0x0D, 0xB3,
|
||||
0x63, 0x72, 0x6A, 0xFF, 0xF6, 0xFB, 0xD9, 0xFF, 0xFD, 0x21, 0x6F, 0xEE, 0x21, 0xAD, 0xFD, 0x43, 0x67, 0x69, 0xC3,
|
||||
0xFB, 0xC7, 0xFF, 0xE8, 0xFF, 0xFD, 0xA0, 0x06, 0xB2, 0xA1, 0x0E, 0x92, 0x61, 0xDB, 0x22, 0x63, 0x72, 0xF8, 0xFB,
|
||||
0x21, 0x65, 0xFB, 0x41, 0x72, 0xFB, 0xAD, 0xA1, 0x0E, 0x92, 0x73, 0xCA, 0x23, 0x65, 0x61, 0x69, 0xC5, 0xFB, 0xC5,
|
||||
0x21, 0x61, 0xBE, 0xC6, 0x0D, 0xB3, 0x72, 0x6C, 0x61, 0x6D, 0x74, 0x6F, 0xFF, 0xD3, 0xFF, 0xEA, 0xFF, 0xED, 0xFF,
|
||||
0xF6, 0xFF, 0xFD, 0xFB, 0x9A, 0xA0, 0x05, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0xC3, 0x05, 0x81, 0x64, 0x65,
|
||||
0x75, 0xFC, 0x8E, 0xFF, 0x9D, 0xFF, 0xFD, 0xC1, 0x0E, 0x92, 0x6F, 0xFF, 0x91, 0x43, 0xA1, 0xA9, 0xB3, 0xFF, 0x8B,
|
||||
0xFF, 0x8B, 0xFF, 0x8B, 0x45, 0x61, 0x65, 0x6C, 0x6F, 0xC3, 0xFF, 0x81, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, 0x81, 0xFF,
|
||||
0xF6, 0x42, 0x61, 0x6F, 0xFF, 0x71, 0xFF, 0x71, 0x41, 0x72, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41, 0x72, 0xFF, 0x63,
|
||||
0x21, 0x65, 0xFC, 0x41, 0x61, 0xFB, 0x3B, 0x42, 0x6D, 0x74, 0xFB, 0x37, 0xFF, 0xFC, 0xC7, 0x0D, 0xB3, 0x6E, 0x67,
|
||||
0x6C, 0x7A, 0x6D, 0x63, 0x73, 0xFF, 0xB4, 0xFF, 0x63, 0xFF, 0xD0, 0xFF, 0xE0, 0xFF, 0xEB, 0xFF, 0xF2, 0xFF, 0xF9,
|
||||
0x41, 0x65, 0xFF, 0x5B, 0x41, 0x73, 0xFF, 0x8F, 0x21, 0x65, 0xFC, 0xA2, 0x0D, 0xB3, 0x70, 0x72, 0xF5, 0xFD, 0x42,
|
||||
0x61, 0x65, 0xFF, 0x27, 0xFF, 0x39, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x20, 0xFF, 0x95, 0xFF, 0x20, 0xFF, 0x20,
|
||||
0xA2, 0x0D, 0xB3, 0x72, 0x6C, 0xEC, 0xF3, 0xA0, 0x0D, 0xE3, 0xC1, 0x0D, 0xE3, 0x6E, 0xFA, 0xE8, 0xA0, 0x0E, 0x72,
|
||||
0xA1, 0x05, 0x81, 0x69, 0xFD, 0xA1, 0x0D, 0xE3, 0x6E, 0xFB, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA, 0xEA, 0xED,
|
||||
0xFB, 0xEA, 0x41, 0x76, 0xFF, 0x0D, 0x41, 0x6D, 0xFF, 0x09, 0x22, 0x65, 0x6F, 0xF8, 0xFC, 0x48, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xD7, 0xFE, 0xE4, 0xFF, 0x23, 0xFF, 0x8D, 0xFF, 0xB0, 0xFF, 0xCB, 0xFF, 0xE8,
|
||||
0xFF, 0xFB, 0x21, 0x74, 0xE7, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFB, 0xB0, 0x21, 0xBA, 0xFC, 0x22, 0x75, 0xC3, 0xF9,
|
||||
0xFD, 0xA0, 0x01, 0x11, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x21, 0x64, 0xFB, 0xA2,
|
||||
0x04, 0xA2, 0x63, 0x72, 0xEA, 0xFD, 0x21, 0x6C, 0xE8, 0x21, 0x75, 0xFD, 0x21, 0x62, 0xFD, 0xA1, 0x04, 0xC2, 0x6D,
|
||||
0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD, 0xE3, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFD, 0x94, 0xFD, 0xD0, 0xFD, 0xD0, 0xFD, 0xD0, 0xFF, 0xDB, 0xFD, 0xD0, 0xFF,
|
||||
0xF0, 0x22, 0xA1, 0xAD, 0xB4, 0xB4, 0x24, 0x61, 0xC3, 0x65, 0x69, 0xAF, 0xFB, 0xAF, 0xAF, 0x21, 0x62, 0xF7, 0xC2,
|
||||
0x01, 0x11, 0x61, 0x6F, 0xFF, 0xA3, 0xFF, 0xA3, 0x21, 0x62, 0xF7, 0x21, 0xAD, 0xFD, 0xA2, 0x04, 0x91, 0x69, 0xC3,
|
||||
0xEE, 0xFD, 0x41, 0x74, 0xFA, 0x1F, 0x21, 0x72, 0xFC, 0x21, 0x6F, 0xFD, 0xA1, 0x0D, 0xB2, 0x62, 0xFD, 0x41, 0x72,
|
||||
0xFE, 0x63, 0x21, 0x61, 0xFC, 0xA1, 0x0D, 0xB2, 0x74, 0xFD, 0xA0, 0x0D, 0xB2, 0xA0, 0x0E, 0xB2, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xDE, 0xEA,
|
||||
0xEF, 0xEF, 0xEF, 0xF5, 0x42, 0x65, 0x6F, 0xFF, 0x88, 0xFF, 0xF1, 0xC3, 0x00, 0x61, 0x61, 0x6F, 0x72, 0xFD, 0xF4,
|
||||
0xFF, 0x3C, 0xFF, 0xF9, 0x41, 0x72, 0xFB, 0x6B, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xFB, 0x4A, 0x42, 0x6F, 0xC3, 0xFB,
|
||||
0x46, 0xFF, 0xFC, 0x43, 0x72, 0x69, 0x73, 0xFB, 0x3C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42, 0x73, 0x74, 0xFB, 0x32, 0xFB,
|
||||
0x32, 0x41, 0xAD, 0xFB, 0x48, 0x22, 0x69, 0xC3, 0xF5, 0xFC, 0x21, 0x6D, 0xFB, 0x41, 0x6D, 0xFB, 0x1F, 0x21, 0x72,
|
||||
0xFC, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x41, 0x76, 0xFB, 0x10, 0x21, 0xA1, 0xFC, 0xA0, 0x04, 0xE2,
|
||||
0x21, 0x70, 0xFD, 0x23, 0x61, 0xC3, 0x75, 0xF3, 0xF7, 0xFD, 0x21, 0x72, 0xF9, 0x41, 0x69, 0xFB, 0x5E, 0x21, 0x64,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x41, 0xB1, 0xFA, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xBA, 0xFD, 0x23, 0x6F, 0x75, 0xC3, 0xF3,
|
||||
0xFA, 0xFD, 0x41, 0x73, 0xFF, 0x42, 0x21, 0xBA, 0xFC, 0x42, 0x75, 0xC3, 0xFA, 0xF7, 0xFF, 0xFD, 0x41, 0x67, 0xFA,
|
||||
0xD3, 0x41, 0x7A, 0xFE, 0x14, 0x41, 0x6A, 0xFA, 0xC8, 0x23, 0xA9, 0xAD, 0xB3, 0xF4, 0xF8, 0xFC, 0x42, 0x61, 0xC3,
|
||||
0xF9, 0x40, 0xFB, 0x35, 0x42, 0x6D, 0x74, 0xF9, 0x39, 0xF9, 0x39, 0x43, 0x7A, 0x6D, 0x73, 0xFF, 0xF2, 0xFA, 0xAF,
|
||||
0xFF, 0xF9, 0x45, 0x65, 0xC3, 0x69, 0x6F, 0x71, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xF6, 0xFF, 0xDD, 0xFA, 0xA5, 0x41,
|
||||
0xAD, 0xFF, 0x76, 0x42, 0x69, 0xC3, 0xFF, 0x72, 0xFF, 0xFC, 0x43, 0xA1, 0xA9, 0xB3, 0xFA, 0x8A, 0xFA, 0x8A, 0xFA,
|
||||
0x8A, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFA, 0x80, 0xFF, 0xF6, 0xFA, 0x80, 0xFA, 0x80, 0x41, 0x65, 0xFA, 0xD8, 0x21,
|
||||
0x72, 0xFC, 0x42, 0x6E, 0x74, 0xFA, 0xA1, 0xFA, 0x6C, 0xA0, 0x00, 0x40, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||
0xA9, 0xAD, 0xFA, 0x94, 0xFF, 0xFD, 0x22, 0x65, 0xC3, 0xE9, 0xF9, 0x22, 0x61, 0x72, 0xE1, 0xFB, 0x41, 0x67, 0xFF,
|
||||
0x42, 0x41, 0xBA, 0xFF, 0x28, 0x42, 0x75, 0xC3, 0xFF, 0x24, 0xFF, 0xFC, 0xCE, 0x07, 0x62, 0x62, 0x64, 0x66, 0x67,
|
||||
0x63, 0x6A, 0x6C, 0x6E, 0x6D, 0x70, 0x65, 0x71, 0x7A, 0x73, 0xFF, 0x00, 0xFF, 0x1A, 0xFF, 0x27, 0xFF, 0x40, 0xFF,
|
||||
0x57, 0xFF, 0x65, 0xFF, 0x97, 0xFF, 0xAB, 0xFF, 0xBC, 0xFF, 0xEC, 0xFF, 0xF1, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0xF9,
|
||||
0xA0, 0x05, 0x02, 0x49, 0x6F, 0x63, 0x69, 0x66, 0x67, 0x76, 0x61, 0x73, 0x74, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x71,
|
||||
0xFA, 0x0C, 0xFA, 0x0C, 0xFA, 0x0C, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x0C, 0x41, 0x64, 0xF8, 0x73, 0x21, 0x6E, 0xFC,
|
||||
0x21, 0x69, 0xFD, 0xC3, 0x07, 0x62, 0x6E, 0x6D, 0x76, 0xFF, 0xDA, 0xFE, 0xDD, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0xF7,
|
||||
0x21, 0x65, 0xFC, 0x41, 0x61, 0xF9, 0xD3, 0x22, 0x69, 0x67, 0xF9, 0xFC, 0xC4, 0x07, 0x62, 0x62, 0x72, 0x63, 0x6A,
|
||||
0xFE, 0xC1, 0xFF, 0xFB, 0xF9, 0xCA, 0xFA, 0x73, 0x42, 0xA1, 0xB3, 0xF9, 0xBB, 0xF9, 0xBB, 0x43, 0x61, 0xC3, 0x6F,
|
||||
0xF9, 0xB4, 0xFF, 0xF9, 0xF9, 0xB4, 0x42, 0x63, 0x71, 0xFF, 0xF6, 0xF9, 0xAA, 0x42, 0x63, 0x71, 0xFF, 0xD0, 0xF9,
|
||||
0xA3, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xEF, 0xFD, 0xC1, 0x05, 0x81, 0x74, 0xFC, 0x34, 0x41, 0x74, 0xFC, 0x2E,
|
||||
0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x42, 0xA9, 0xB3, 0xF8, 0x05, 0xF8, 0x05, 0x48, 0x72, 0x61, 0x73,
|
||||
0x6D, 0x65, 0xC3, 0x64, 0x66, 0xF7, 0xFE, 0xF7, 0xFE, 0xF7, 0xFE, 0xFF, 0x16, 0xF7, 0xFE, 0xFF, 0xF9, 0xF7, 0xFE,
|
||||
0xF9, 0x7B, 0xC1, 0x05, 0x81, 0x72, 0xF7, 0xE5, 0x42, 0xAD, 0xA1, 0xFF, 0xFA, 0xF7, 0xDF, 0x43, 0x69, 0xC3, 0x74,
|
||||
0xFF, 0xDA, 0xFF, 0xF9, 0xF9, 0x55, 0x41, 0xA1, 0xF9, 0x4E, 0x42, 0x61, 0xC3, 0xF9, 0x4A, 0xFF, 0xFC, 0x41, 0x7A,
|
||||
0xF9, 0x40, 0x21, 0xAD, 0xFC, 0x22, 0x69, 0xC3, 0xF9, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x69, 0xFD, 0xC5, 0x07, 0x62,
|
||||
0x62, 0x6D, 0x6E, 0x73, 0x74, 0xFF, 0x95, 0xFF, 0xA7, 0xFF, 0xD9, 0xFF, 0xE7, 0xFF, 0xFD, 0x43, 0x61, 0x65, 0x6F,
|
||||
0xF9, 0x1C, 0xF9, 0x1C, 0xF9, 0x1C, 0xC2, 0x07, 0x82, 0x62, 0x6D, 0xF9, 0x15, 0xFF, 0xF6, 0x45, 0xA1, 0xA9, 0xAD,
|
||||
0xB3, 0xBA, 0xFF, 0xF7, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9, 0xF0, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3,
|
||||
0xFE, 0xBD, 0xFE, 0xEA, 0xFF, 0x13, 0xFF, 0x2F, 0xFF, 0xCB, 0xFF, 0xF0, 0xA1, 0x00, 0x61, 0x65, 0xED, 0x43, 0x6E,
|
||||
0x72, 0x73, 0xFE, 0xD2, 0xF8, 0xE1, 0xF8, 0xE1, 0xA0, 0x0C, 0x12, 0xA1, 0x0C, 0x12, 0x72, 0xFD, 0x21, 0x6E, 0xF8,
|
||||
0x21, 0xB3, 0xFD, 0x23, 0x61, 0x6F, 0xC3, 0xF2, 0xF5, 0xFD, 0x41, 0x70, 0xF7, 0x45, 0x41, 0x6E, 0xF7, 0x41, 0x21,
|
||||
0x65, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x74, 0xEC, 0xFD, 0x41, 0xA9, 0xF8,
|
||||
0xBA, 0x21, 0x65, 0xD6, 0x21, 0x69, 0xFD, 0xC7, 0x0F, 0x93, 0x65, 0x6C, 0x64, 0x6E, 0x72, 0xC3, 0x6D, 0xFF, 0xBE,
|
||||
0xF9, 0x7B, 0xFF, 0xD6, 0xFF, 0xF1, 0xF8, 0x9F, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0xA1, 0xFC, 0xEB, 0x21, 0xC3, 0xFC,
|
||||
0x42, 0x70, 0x74, 0xF8, 0xA9, 0xF7, 0x03, 0x22, 0x75, 0x65, 0xF6, 0xF9, 0x41, 0xA1, 0xF8, 0x74, 0x44, 0x61, 0xC3,
|
||||
0x65, 0x6F, 0xF8, 0x70, 0xFF, 0xFC, 0xF8, 0x70, 0xF8, 0x70, 0x21, 0x74, 0xF3, 0x41, 0x65, 0xF6, 0xE3, 0x21, 0x75,
|
||||
0xFC, 0x21, 0x6C, 0xFD, 0x41, 0x61, 0xFA, 0xF6, 0x41, 0x6E, 0xFC, 0xB6, 0x21, 0x65, 0xFC, 0x21, 0x6D, 0xFD, 0x41,
|
||||
0x65, 0xF8, 0x4B, 0x23, 0x63, 0x69, 0x74, 0xEE, 0xF9, 0xFC, 0x41, 0x69, 0xF8, 0x66, 0x21, 0x6D, 0xFC, 0x21, 0xB3,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0xC6, 0x0F, 0x93, 0x63, 0x73, 0x66, 0x6C, 0x72, 0x74, 0xFF, 0xB7, 0xFF, 0xCD, 0xFF, 0xD7,
|
||||
0xFF, 0xEC, 0xFB, 0x06, 0xFF, 0xFD, 0x41, 0x75, 0xFC, 0x7F, 0x21, 0x63, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x6D, 0xFF,
|
||||
0x57, 0x21, 0x65, 0xFC, 0x21, 0x6C, 0xAA, 0x21, 0x70, 0xFD, 0x41, 0x74, 0xFF, 0x4A, 0x41, 0x65, 0xF8, 0x29, 0x41,
|
||||
0x6D, 0xF6, 0x7F, 0x21, 0xAD, 0xFC, 0x41, 0x75, 0xF8, 0x1E, 0x44, 0x61, 0x69, 0xC3, 0x72, 0xF8, 0x1A, 0xFF, 0xF5,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0x22, 0x70, 0x74, 0xE4, 0xF3, 0xA5, 0x0F, 0x93, 0x6A, 0x6C, 0x6D, 0x6E, 0x73, 0xCB, 0xD2,
|
||||
0xD8, 0xDB, 0xFB, 0x41, 0x69, 0xFC, 0x36, 0x21, 0x70, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x63, 0xFA,
|
||||
0x65, 0x21, 0x69, 0xFC, 0x41, 0xAD, 0xF7, 0xCF, 0x42, 0x69, 0xC3, 0xF7, 0xCB, 0xFF, 0xFC, 0x21, 0x64, 0xF9, 0xA3,
|
||||
0x0F, 0x93, 0x63, 0x66, 0x72, 0xE8, 0xEF, 0xFD, 0x41, 0x62, 0xFA, 0xEF, 0xA1, 0x0F, 0x93, 0x72, 0xFC, 0x42, 0xA9,
|
||||
0xB3, 0xF7, 0x9E, 0xF7, 0x9E, 0x42, 0x61, 0xC3, 0xF7, 0x97, 0xFF, 0xF9, 0x21, 0x74, 0xF9, 0x41, 0x74, 0xFF, 0x50,
|
||||
0xA2, 0x0F, 0xC3, 0x73, 0x72, 0xF9, 0xFC, 0xA0, 0x0F, 0xC3, 0xC3, 0x0F, 0xC3, 0x6E, 0x6D, 0x72, 0xFD, 0x8F, 0xFA,
|
||||
0x1F, 0xF7, 0x7F, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA, 0xF1, 0xF4, 0xF1, 0xF1, 0x41, 0x79, 0xF8, 0x11, 0x21,
|
||||
0x61, 0xFC, 0x48, 0x69, 0x68, 0x61, 0x65, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xC2, 0xF8, 0x91, 0xFF, 0x31, 0xFF, 0x82,
|
||||
0xFF, 0xB1, 0xFF, 0xBE, 0xFF, 0xEE, 0xFF, 0xFD, 0x41, 0x74, 0xF8, 0x78, 0x21, 0x73, 0xFC, 0x41, 0x73, 0xF8, 0x71,
|
||||
0x21, 0x65, 0xFC, 0x22, 0x65, 0x6F, 0xF6, 0xFD, 0x22, 0x62, 0x72, 0xD4, 0xFB, 0x41, 0x73, 0xFD, 0x21, 0x21, 0x61,
|
||||
0xFC, 0x41, 0x61, 0xFD, 0x39, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62,
|
||||
0xFD, 0xA3, 0x00, 0x61, 0x75, 0x6F, 0x65, 0xE1, 0xEA, 0xFD, 0x41, 0x70, 0xF6, 0x09, 0x21, 0x6D, 0xFC, 0x41, 0x6A,
|
||||
0xF6, 0x02, 0xA0, 0x05, 0x52, 0x21, 0x74, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0x22, 0x62, 0x6C, 0xF0, 0xFD,
|
||||
0x22, 0x69, 0x6F, 0xE8, 0xFB, 0x21, 0x65, 0xFB, 0x21, 0x6C, 0xFD, 0xC1, 0x0E, 0xB2, 0x67, 0xF9, 0x8A, 0x41, 0x67,
|
||||
0xF5, 0x63, 0xA1, 0x0E, 0xB2, 0x65, 0xFC, 0x41, 0xB1, 0xF9, 0x7B, 0xA1, 0x0E, 0xB2, 0xC3, 0xFC, 0x43, 0xA1, 0xA9,
|
||||
0xB3, 0xF5, 0x51, 0xF5, 0x51, 0xF5, 0x51, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xF5, 0x47, 0xFF, 0xF6, 0xF5, 0x47, 0xF5,
|
||||
0x47, 0x21, 0x74, 0xF3, 0xC2, 0x0E, 0xB2, 0x64, 0x6E, 0xFA, 0x38, 0xFF, 0xFD, 0xA0, 0x10, 0xD2, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF9, 0x3A, 0xFB,
|
||||
0x1F, 0xFF, 0xB7, 0xFF, 0xC1, 0xFF, 0xCA, 0xFF, 0xE9, 0xFF, 0xF5, 0x21, 0x73, 0xEA, 0x41, 0x78, 0xF8, 0x7E, 0x21,
|
||||
0xB3, 0xFC, 0x21, 0xC3, 0xFD, 0x22, 0x61, 0x69, 0xF3, 0xFD, 0xC2, 0x00, 0x61, 0x65, 0x72, 0xFF, 0x8C, 0xFF, 0xFB,
|
||||
0x42, 0x61, 0x6F, 0xF6, 0x48, 0xF6, 0x48, 0x21, 0x65, 0xF9, 0x21, 0x6D, 0xFD, 0x41, 0x65, 0xF6, 0x3B, 0x21, 0x65,
|
||||
0xFC, 0x21, 0x6D, 0xFD, 0x22, 0x75, 0x65, 0xF3, 0xFD, 0x41, 0x65, 0xF5, 0x60, 0x21, 0x72, 0xFC, 0x41, 0x6F, 0xF5,
|
||||
0x59, 0x21, 0x72, 0xFC, 0x41, 0x72, 0xFB, 0x39, 0x21, 0x67, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x68,
|
||||
0xFB, 0x2C, 0x21, 0x6F, 0xFC, 0x21, 0x63, 0xFD, 0x41, 0x69, 0xF6, 0x72, 0x21, 0x6E, 0xFC, 0x41, 0x72, 0xF6, 0x6B,
|
||||
0x23, 0x6C, 0x6D, 0x65, 0xF2, 0xF9, 0xFC, 0x41, 0xB3, 0xFC, 0x0A, 0x42, 0x6F, 0xC3, 0xFC, 0x06, 0xFF, 0xFC, 0x21,
|
||||
0x73, 0xF9, 0xA0, 0x05, 0x22, 0x21, 0x65, 0xFD, 0x42, 0x6A, 0x6E, 0xFF, 0xFD, 0xF9, 0x78, 0x21, 0x6F, 0xF9, 0x42,
|
||||
0x65, 0x69, 0xFF, 0xFD, 0xF5, 0x0B, 0x24, 0x65, 0x61, 0x69, 0x74, 0xBC, 0xD4, 0xE6, 0xF9, 0x41, 0x74, 0xFF, 0xA2,
|
||||
0xC4, 0x02, 0xB1, 0x62, 0x63, 0x6E, 0x74, 0xFF, 0x9B, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFC, 0x41, 0x74, 0xF6, 0xD3,
|
||||
0x21, 0x73, 0xFC, 0xA1, 0x01, 0x82, 0x65, 0xFD, 0x42, 0x6F, 0xC3, 0xF8, 0xA2, 0xFA, 0x85, 0x41, 0x69, 0xF5, 0xE2,
|
||||
0x21, 0x65, 0xFC, 0x41, 0xA9, 0xFD, 0x00, 0x42, 0x65, 0xC3, 0xFC, 0xFC, 0xFF, 0xFC, 0x41, 0x62, 0xF5, 0xB3, 0xA4,
|
||||
0x09, 0xA3, 0x6D, 0x63, 0x6A, 0x72, 0xE3, 0xEE, 0xF5, 0xFC, 0x41, 0xAD, 0xFA, 0xC6, 0x42, 0x69, 0xC3, 0xFA, 0xC2,
|
||||
0xFF, 0xFC, 0xA1, 0x09, 0xA3, 0x6D, 0xF9, 0xA0, 0x09, 0xA3, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFC, 0x2F, 0xFE, 0xC3,
|
||||
0xF4, 0x14, 0xF4, 0x14, 0xA1, 0x09, 0xA3, 0x6A, 0xF3, 0x43, 0x61, 0xC3, 0x65, 0xF4, 0x02, 0xF5, 0xF7, 0xF4, 0x02,
|
||||
0x21, 0x72, 0xF6, 0x21, 0x65, 0xFD, 0xA1, 0x09, 0xA3, 0x6D, 0xFD, 0xA0, 0x09, 0xD3, 0xC1, 0x09, 0xD3, 0x6A, 0xF6,
|
||||
0x40, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF7, 0xF7, 0xF7, 0xFA, 0xF7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xC3, 0xFF, 0x85, 0xFF, 0xA7, 0xFF, 0xBD, 0xFF, 0xC2, 0xFF, 0xD2, 0xFF, 0xE7, 0xFF, 0xF5, 0x41, 0x73, 0xF6, 0x3B,
|
||||
0x42, 0x6C, 0x75, 0xF6, 0x37, 0xFF, 0xFC, 0x41, 0x6C, 0xF6, 0x30, 0x41, 0x72, 0xFF, 0x59, 0x41, 0x6D, 0xF6, 0x28,
|
||||
0x44, 0xA1, 0xAD, 0xB3, 0xBA, 0xFF, 0xF4, 0xF6, 0x27, 0xFF, 0xF8, 0xFF, 0xFC, 0xC5, 0x01, 0x82, 0x61, 0xC3, 0x69,
|
||||
0x6F, 0x75, 0xFF, 0xE0, 0xFF, 0xF3, 0xF6, 0x1A, 0xFF, 0xEB, 0xFF, 0xEF, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF,
|
||||
0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFF, 0xDE,
|
||||
0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0xF0, 0x42, 0x6E, 0x78, 0xFF, 0x8E, 0xFF, 0xEA,
|
||||
0xA0, 0x01, 0x82, 0x41, 0x72, 0xF5, 0x3F, 0x41, 0x72, 0xF5, 0x0B, 0x22, 0x61, 0x6F, 0xF8, 0xFC, 0x42, 0x6E, 0x70,
|
||||
0xF4, 0xEA, 0xF4, 0xEA, 0x21, 0x65, 0xF9, 0x41, 0x70, 0xF4, 0xE0, 0x22, 0x61, 0x6F, 0xFC, 0xFC, 0x41, 0x61, 0xFA,
|
||||
0xE0, 0x21, 0x75, 0xFC, 0x41, 0x6D, 0xFF, 0x00, 0x21, 0xA1, 0xFC, 0x41, 0x65, 0xF4, 0xBD, 0x22, 0xC3, 0x69, 0xF9,
|
||||
0xFC, 0x41, 0x69, 0xFB, 0x63, 0x21, 0x6C, 0xFC, 0x42, 0x6D, 0x63, 0xF4, 0x9C, 0xF3, 0x1F, 0x22, 0x61, 0x69, 0xF6,
|
||||
0xF9, 0x41, 0x61, 0xFE, 0xDD, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xF4, 0x89, 0x42, 0x61, 0x69, 0xFB, 0x45, 0xF4, 0xEA,
|
||||
0x43, 0x63, 0x68, 0x6E, 0xF4, 0xEC, 0xFF, 0xD2, 0xF4, 0xFD, 0x21, 0x65, 0xF6, 0x24, 0x61, 0x65, 0x6C, 0x72, 0xE5,
|
||||
0xE8, 0xEC, 0xFD, 0x41, 0x63, 0xF4, 0x85, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xF4, 0x72, 0x21, 0xC3, 0xFC, 0x41, 0x67,
|
||||
0xF4, 0x5A, 0x21, 0x75, 0xFC, 0x22, 0x6D, 0x72, 0xF6, 0xFD, 0x41, 0x69, 0xF4, 0x6E, 0x41, 0x62, 0xF2, 0xCD, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x76, 0xFD, 0x21, 0x6F, 0xFD, 0xCC, 0x09, 0xA3, 0x62, 0x63, 0x64, 0x67, 0x6C, 0x6E, 0x70, 0x66,
|
||||
0x72, 0x73, 0x74, 0x6D, 0xFF, 0x6B, 0xFF, 0x77, 0xFF, 0x7E, 0xFF, 0x87, 0xFF, 0x95, 0xFF, 0xA8, 0xFF, 0xCC, 0xFF,
|
||||
0xD9, 0xFF, 0xEA, 0xFF, 0xEF, 0xFA, 0x67, 0xFF, 0xFD, 0xC1, 0x02, 0x91, 0x69, 0xF4, 0x16, 0x21, 0x63, 0xFA, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x64, 0xF4, 0x78, 0x22, 0x75, 0x65, 0xFC, 0xAC, 0x41, 0x6F, 0xFA, 0x27, 0x42, 0x63, 0x61, 0xFF,
|
||||
0xFC, 0xF8, 0x70, 0x41, 0x76, 0xF2, 0x79, 0x21, 0xAD, 0xFC, 0x42, 0x69, 0xC3, 0xF4, 0x24, 0xFF, 0xFD, 0x21, 0x75,
|
||||
0xF9, 0xC2, 0x02, 0x91, 0x61, 0x68, 0xFF, 0x7D, 0xFA, 0x12, 0xC6, 0x09, 0xA3, 0x66, 0x6C, 0x6E, 0x71, 0x78, 0x76,
|
||||
0xFF, 0xCF, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xF7, 0xFE, 0x17, 0x42, 0x6F, 0x61, 0xF2, 0x4A, 0xF2, 0x4A,
|
||||
0x21, 0x75, 0xF9, 0x41, 0x6C, 0xFF, 0x2D, 0x21, 0x61, 0xFC, 0x21, 0x75, 0xFD, 0xC3, 0x09, 0xA3, 0x63, 0x67, 0x6E,
|
||||
0xFF, 0xF3, 0xFF, 0xFD, 0xF3, 0xB3, 0x41, 0x73, 0xFB, 0x5F, 0x44, 0x74, 0x61, 0xC3, 0x65, 0xF3, 0xA3, 0xF2, 0x26,
|
||||
0xF4, 0x1B, 0xF2, 0x26, 0x43, 0x6F, 0x61, 0x6C, 0xF2, 0x19, 0xF2, 0x19, 0xFF, 0xF3, 0x42, 0x63, 0x74, 0xF2, 0x0F,
|
||||
0xF2, 0x0F, 0x21, 0x6E, 0xF9, 0x22, 0x75, 0x65, 0xEC, 0xFD, 0x41, 0x73, 0xF2, 0x00, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0x41, 0x6F, 0xF8, 0x25, 0xA4, 0x09, 0xA3, 0x62, 0x63, 0x66, 0x70, 0xC8, 0xED, 0xF9, 0xFC, 0x41, 0x7A, 0xF1,
|
||||
0xE7, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x09, 0xA3, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x2B,
|
||||
0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3, 0x6E, 0xFD, 0x41, 0x74, 0xF4, 0x1F, 0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3, 0x64,
|
||||
0xFD, 0x41, 0x69, 0xF4, 0x16, 0xA1, 0x09, 0xD3, 0x74, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xE6, 0xFF,
|
||||
0xF2, 0xFD, 0xC7, 0xFD, 0xC7, 0xFF, 0xFB, 0xA0, 0x0C, 0x13, 0x21, 0x67, 0xFD, 0x22, 0x63, 0x74, 0xFA, 0xFA, 0x21,
|
||||
0x70, 0xF5, 0x22, 0x70, 0x6D, 0xF8, 0xFD, 0xA2, 0x05, 0x22, 0x6F, 0x75, 0xF0, 0xFB, 0xA0, 0x0A, 0x92, 0xA0, 0x0A,
|
||||
0xB3, 0xA0, 0x0B, 0x13, 0x23, 0xA1, 0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6,
|
||||
0xF9, 0xA1, 0x0A, 0xB3, 0x73, 0xF7, 0xA0, 0x0A, 0xE3, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0x28, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xD4, 0xD7, 0xED, 0xD7, 0xD7, 0xD7, 0xF5, 0x21,
|
||||
0x72, 0xEF, 0x21, 0x65, 0xFD, 0x48, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x74, 0xFD, 0xE7, 0xFE, 0x87, 0xFE,
|
||||
0xE8, 0xFF, 0x11, 0xFF, 0x55, 0xFF, 0x6D, 0xFF, 0x93, 0xFF, 0xFD, 0x21, 0x6E, 0xE7, 0x58, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x68, 0x61, 0x65,
|
||||
0x69, 0xF2, 0x79, 0xF3, 0xD1, 0xF4, 0x53, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0xC4,
|
||||
0xF4, 0x5A, 0xF7, 0x4E, 0xF4, 0x5A, 0xF9, 0xC2, 0xFB, 0x92, 0xFC, 0x33, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4,
|
||||
0x5A, 0xF4, 0x5A, 0xFC, 0x53, 0xFC, 0xC1, 0xFD, 0xC4, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x16, 0xA0, 0x0D, 0x22, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xC3, 0x00, 0x71, 0x2E, 0x69, 0x65, 0xF0, 0x3F, 0xFF, 0xF3, 0xFF, 0xFD, 0xC3, 0x00,
|
||||
0x71, 0x2E, 0x7A, 0x73, 0xF0, 0x33, 0xF0, 0x39, 0xF0, 0x39, 0xC1, 0x00, 0x71, 0x2E, 0xF0, 0x27, 0xD6, 0x00, 0x81,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xF0, 0x21, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24,
|
||||
0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0,
|
||||
0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0x41, 0x74, 0xF0, 0x69, 0x21, 0x70, 0xFC, 0xD7, 0x00, 0x91,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x65, 0xEF, 0xD5, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0,
|
||||
0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01,
|
||||
0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xFF, 0xFD, 0x42, 0x6F, 0x70, 0xF3, 0xA8, 0xFF, 0xB1,
|
||||
0x41, 0x6E, 0xFB, 0x50, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x69, 0x6F, 0xEF, 0x82, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF,
|
||||
0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE,
|
||||
0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xFF,
|
||||
0xF5, 0xFF, 0xFC, 0x41, 0x74, 0xF1, 0xF3, 0x21, 0x70, 0xFC, 0x41, 0x6F, 0xFC, 0x90, 0x21, 0x6C, 0xFC, 0x41, 0x74,
|
||||
0xF0, 0x5B, 0x41, 0x6D, 0xFA, 0xEF, 0x41, 0x61, 0xEF, 0x9F, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x25, 0x6D, 0x75,
|
||||
0x72, 0x73, 0x6E, 0xE4, 0xEB, 0xEE, 0xF2, 0xFD, 0xA0, 0x02, 0x32, 0x21, 0x61, 0xFD, 0x41, 0x2E, 0xEF, 0x06, 0xA1,
|
||||
0x02, 0x32, 0x73, 0xFC, 0x22, 0x6F, 0x61, 0xF1, 0xFB, 0x41, 0x64, 0xEF, 0x88, 0x23, 0x63, 0x67, 0x72, 0xEB, 0xF7,
|
||||
0xFC, 0x21, 0x6F, 0xE1, 0x41, 0x75, 0xEF, 0x68, 0x21, 0x72, 0xFC, 0x42, 0x64, 0x73, 0xFF, 0xFD, 0xF2, 0xE9, 0x22,
|
||||
0x6C, 0x61, 0xEF, 0xF9, 0x41, 0x6C, 0xEF, 0x64, 0x21, 0x61, 0xFC, 0xA0, 0x07, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0xA1, 0x04, 0x72, 0x72, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xEF, 0xD7, 0xEF, 0xD7, 0xEF,
|
||||
0xD7, 0xEF, 0xD7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEF, 0xC1, 0xEF, 0xC4, 0xEF, 0xC4, 0xEF, 0xC4,
|
||||
0xEF, 0xC4, 0xEF, 0xC4, 0xFF, 0xF0, 0x21, 0x69, 0xEA, 0x21, 0x74, 0xFD, 0x22, 0x66, 0x6E, 0xC3, 0xFD, 0x42, 0x68,
|
||||
0x6F, 0xF2, 0x5F, 0xEF, 0xB4, 0x21, 0x6E, 0xF9, 0xA0, 0x06, 0xF3, 0xA0, 0x07, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF3, 0x4F, 0xF3, 0x26,
|
||||
0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xF5, 0x21, 0x72, 0xE7, 0x21, 0x65, 0xFD, 0x41,
|
||||
0x6C, 0xFA, 0x21, 0x41, 0x6F, 0xF2, 0x6E, 0x24, 0x61, 0x62, 0x63, 0x74, 0xC5, 0xF5, 0xF8, 0xFC, 0x41, 0x6F, 0xEF,
|
||||
0x25, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0xDC, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x68, 0x6C, 0x72, 0x6F, 0x61, 0x75, 0x65, 0x69, 0xC3, 0xEE, 0x30, 0xEE, 0x33, 0xEE, 0x39, 0xEE,
|
||||
0x33, 0xEE, 0x42, 0xEE, 0x47, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x47, 0xFD, 0xF1, 0xEE, 0x4C, 0xEE, 0x33, 0xEE, 0x33,
|
||||
0xFD, 0xFD, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xFE, 0x09, 0xFE, 0x0F, 0xFE, 0x5B, 0xFE, 0xAE, 0xFF,
|
||||
0x19, 0xFF, 0x3C, 0xFF, 0x54, 0xFF, 0x9A, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x74, 0xF2, 0xB2, 0x21, 0x6E, 0xFC, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF1,
|
||||
0x95, 0xF1, 0xD1, 0xF1, 0xD1, 0xFF, 0xFB, 0xF1, 0xD1, 0xF1, 0xD1, 0xF1, 0xD7, 0x21, 0x61, 0xEA, 0xA0, 0x07, 0xC1,
|
||||
0xA0, 0x07, 0xD2, 0xA0, 0x07, 0xF2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x74, 0xFD,
|
||||
0x42, 0x61, 0x6F, 0xFF, 0xFD, 0xEE, 0xA8, 0x21, 0x6D, 0xF9, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0xA0, 0x09, 0x32, 0x21, 0x61, 0xFD, 0x22, 0x72, 0x6C, 0xF7, 0xFD, 0xA0, 0x02, 0x12, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xEF, 0x4A, 0x22, 0x68, 0x6C, 0xF9, 0xFC, 0x22, 0x61, 0x65, 0xE0, 0xE0, 0x21,
|
||||
0x68, 0xFB, 0x21, 0x74, 0xE3, 0x22, 0x63, 0x72, 0xFA, 0xFD, 0x23, 0xB3, 0xA1, 0xA9, 0xD6, 0xEB, 0xFB, 0x21, 0x6A,
|
||||
0xC0, 0x21, 0x6C, 0xBD, 0x21, 0x74, 0xBA, 0x21, 0x6E, 0xFD, 0x22, 0x6C, 0x65, 0xF7, 0xFD, 0xA0, 0x02, 0x11, 0x21,
|
||||
0x6A, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x69, 0xFF, 0xA6, 0x21, 0x6E, 0xFC, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0x9C,
|
||||
0x21, 0x61, 0xFC, 0x46, 0x64, 0x65, 0x69, 0x74, 0x67, 0x6E, 0xFF, 0x98, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xEC, 0xFF,
|
||||
0xF6, 0xFF, 0xFD, 0x42, 0x63, 0x7A, 0xFF, 0x82, 0xFF, 0x82, 0x41, 0x6E, 0xFF, 0x7B, 0x21, 0x65, 0xFC, 0x22, 0x65,
|
||||
0x69, 0xF2, 0xFD, 0x21, 0x64, 0xFB, 0x41, 0x67, 0xFF, 0x6C, 0x21, 0x69, 0xFC, 0x41, 0x72, 0xFF, 0x65, 0x21, 0x74,
|
||||
0xFC, 0x23, 0x65, 0x6C, 0x73, 0xEF, 0xF6, 0xFD, 0xA0, 0x09, 0x12, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFF, 0x51, 0x21,
|
||||
0xBA, 0xFC, 0x23, 0x61, 0x75, 0xC3, 0xF6, 0xF9, 0xFD, 0x21, 0x6F, 0xDE, 0xC2, 0x09, 0x12, 0x63, 0x64, 0xFF, 0x91,
|
||||
0xFF, 0x91, 0x23, 0xA1, 0xA9, 0xB3, 0xE0, 0xE0, 0xE0, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x6C, 0xFF, 0xF0, 0xFF, 0xF9,
|
||||
0xFF, 0xD9, 0xFF, 0xD9, 0xFF, 0x81, 0x41, 0x69, 0xFF, 0x84, 0x21, 0x72, 0xFC, 0x41, 0x65, 0xFF, 0x6A, 0x21, 0x63,
|
||||
0xFC, 0x42, 0xA1, 0xA9, 0xFF, 0x12, 0xFF, 0x12, 0x43, 0x61, 0xC3, 0x69, 0xFF, 0x0B, 0xFF, 0xF9, 0xFF, 0x0B, 0x41,
|
||||
0xA9, 0xFF, 0x01, 0x42, 0x65, 0xC3, 0xFE, 0xFD, 0xFF, 0xFC, 0x41, 0x67, 0xFF, 0x0A, 0x21, 0x65, 0xFC, 0x4B, 0x72,
|
||||
0x62, 0x63, 0x64, 0x6C, 0x70, 0x6E, 0x76, 0x78, 0x79, 0x73, 0xFF, 0x5A, 0xFF, 0x91, 0xFF, 0xA5, 0xFF, 0xAC, 0xFF,
|
||||
0xBF, 0xFF, 0xD3, 0xFF, 0xDA, 0xFF, 0xE4, 0xFF, 0x49, 0xFF, 0xF2, 0xFF, 0xFD, 0xA0, 0x08, 0xC3, 0x21, 0x64, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x72, 0xF7, 0x22, 0x72, 0x6F, 0xFA, 0xFD, 0x22, 0x7A, 0x63, 0xEF, 0xEF, 0x21, 0x69, 0xFB,
|
||||
0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0xA1, 0xA9, 0xB3, 0xDE, 0xDE, 0xDE, 0x22, 0x61, 0xC3,
|
||||
0xD7, 0xF9, 0x23, 0x61, 0x65, 0x6F, 0xD2, 0xD2, 0xD2, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xF1, 0xFD, 0xA0, 0x08,
|
||||
0xF2, 0x21, 0x73, 0xC0, 0x22, 0x61, 0x69, 0xFA, 0xFD, 0x21, 0x75, 0xFB, 0x21, 0xAD, 0xC6, 0x22, 0x69, 0xC3, 0xC3,
|
||||
0xFD, 0x42, 0x76, 0x6E, 0xFF, 0xAD, 0xFF, 0xFB, 0x42, 0x61, 0x69, 0xED, 0xDD, 0xFF, 0xF9, 0x41, 0x6C, 0xFE, 0x80,
|
||||
0x42, 0x72, 0x65, 0xFE, 0x7C, 0xFF, 0xFC, 0x21, 0x67, 0xF9, 0x41, 0x76, 0xFF, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x73,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x6C, 0xFF, 0x7E, 0x21, 0x6C, 0xFC, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x41, 0x72, 0xFE, 0x52, 0x43, 0x61, 0x65, 0x74, 0xF1, 0xB9, 0xF1, 0xB9, 0xFF, 0xFC, 0x41,
|
||||
0x73, 0xFF, 0xA0, 0x21, 0x65, 0xFC, 0x41, 0x6E, 0xFF, 0x5C, 0x21, 0x75, 0xFC, 0x21, 0xB3, 0xF9, 0x22, 0xC3, 0x6F,
|
||||
0xFD, 0xF6, 0x4D, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x79, 0x7A, 0xFF, 0x59, 0xFF,
|
||||
0x6C, 0xFF, 0x85, 0xFF, 0x95, 0xFE, 0x37, 0xFF, 0xA7, 0xFF, 0xB9, 0xFF, 0xCC, 0xFF, 0xD9, 0xFF, 0xE0, 0xFF, 0xEE,
|
||||
0xFF, 0xF5, 0xFF, 0xFB, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x25, 0xFF, 0x47, 0xFF, 0x25, 0xFF, 0x25, 0x21, 0x6A,
|
||||
0xF3, 0x41, 0x6A, 0xFF, 0x43, 0x21, 0xA9, 0xFC, 0x41, 0xB1, 0xFD, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xA9, 0xFD, 0x22,
|
||||
0x65, 0xC3, 0xFA, 0xFD, 0x23, 0x65, 0xC3, 0x70, 0xE7, 0xEE, 0xFB, 0x41, 0x6E, 0xFD, 0xD9, 0x21, 0xA9, 0xFC, 0x22,
|
||||
0x65, 0xC3, 0xF9, 0xFD, 0x21, 0x72, 0xFB, 0x21, 0x66, 0xFD, 0xC6, 0x02, 0x11, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73,
|
||||
0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0x46, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73,
|
||||
0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0x21, 0xA1, 0xED, 0xC6, 0x09, 0x12, 0x6E,
|
||||
0x72, 0x62, 0x64, 0x6D, 0x73, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0x42, 0xA1,
|
||||
0xB3, 0xFF, 0xEB, 0xFE, 0x1C, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFE, 0x15, 0xFE, 0x35, 0xFE, 0x15, 0xFE, 0x15, 0x44,
|
||||
0x6F, 0x61, 0xC3, 0x68, 0xFE, 0x08, 0xFF, 0xD7, 0xFF, 0xEC, 0xFF, 0xF3, 0x41, 0xA9, 0xFE, 0x85, 0x42, 0x65, 0xC3,
|
||||
0xFE, 0x81, 0xFF, 0xFC, 0xA1, 0x05, 0x92, 0x75, 0xF9, 0x41, 0x66, 0xFD, 0x42, 0x42, 0x63, 0x71, 0xFD, 0x3E, 0xFD,
|
||||
0x3E, 0x22, 0x69, 0x75, 0xF5, 0xF9, 0x41, 0x62, 0xFD, 0xCD, 0x21, 0x6D, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x7A, 0xFD,
|
||||
0x28, 0x42, 0x63, 0x6E, 0xFD, 0x75, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0x21, 0x72, 0xFD, 0x44, 0x61, 0x65, 0x69, 0x75,
|
||||
0xFD, 0x17, 0xFF, 0xFD, 0xFD, 0x9C, 0xFD, 0x7B, 0x41, 0x69, 0xFD, 0x4D, 0x41, 0x69, 0xFD, 0x8B, 0x43, 0x62, 0x63,
|
||||
0x6C, 0xFF, 0xF8, 0xFD, 0x5C, 0xFF, 0xFC, 0x41, 0x73, 0xFC, 0xF8, 0x41, 0x63, 0xFC, 0xF4, 0x22, 0x65, 0x75, 0xF8,
|
||||
0xFC, 0x43, 0x61, 0x69, 0x72, 0xFF, 0xE9, 0xFD, 0x4F, 0xFF, 0xFB, 0x23, 0x63, 0x70, 0x74, 0xB6, 0xCA, 0xF6, 0x42,
|
||||
0x63, 0x74, 0xFC, 0xF1, 0xFC, 0xEE, 0x4A, 0x6D, 0x6E, 0x6F, 0x61, 0xC3, 0x63, 0x71, 0x64, 0x73, 0x72, 0xFF, 0x07,
|
||||
0xFF, 0x1D, 0xFD, 0x24, 0xFF, 0x20, 0xFF, 0x48, 0xFF, 0x74, 0xFF, 0x8C, 0xFF, 0x9C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42,
|
||||
0x72, 0x6F, 0xFD, 0x05, 0xFC, 0xF7, 0x42, 0x61, 0x6F, 0xFC, 0xFE, 0xFC, 0xFE, 0x22, 0x65, 0x69, 0xF2, 0xF9, 0x41,
|
||||
0x74, 0xFC, 0xF2, 0x21, 0x72, 0xFC, 0x41, 0xA1, 0xFC, 0xDD, 0x42, 0x61, 0xC3, 0xFC, 0xD9, 0xFF, 0xFC, 0x42, 0x6E,
|
||||
0x75, 0xFC, 0xE0, 0xFF, 0xF9, 0x41, 0xB3, 0xFD, 0x0D, 0x42, 0x6F, 0xC3, 0xFD, 0x09, 0xFF, 0xFC, 0x21, 0x69, 0xF9,
|
||||
0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x42, 0x67, 0x6E, 0xFF, 0x6E, 0xFC, 0x74, 0x41, 0x65, 0xFF, 0x75, 0x42, 0x6F,
|
||||
0x72, 0xFC, 0xEE, 0xFF, 0xFC, 0x22, 0x61, 0x70, 0xEE, 0xF9, 0x41, 0x72, 0xFD, 0x0C, 0x41, 0x73, 0xFC, 0x9F, 0x21,
|
||||
0x75, 0xFC, 0x44, 0x65, 0x6C, 0x6F, 0x72, 0xFC, 0x9B, 0xFF, 0x4C, 0xFF, 0xF5, 0xFF, 0xFD, 0x42, 0x63, 0x74, 0xFC,
|
||||
0xEE, 0xFC, 0xEE, 0x21, 0x6E, 0xF9, 0x41, 0x63, 0xFC, 0x8C, 0x41, 0x72, 0xFC, 0x7D, 0xC1, 0x05, 0x92, 0x61, 0xFC,
|
||||
0x97, 0x41, 0x72, 0xFC, 0x91, 0x24, 0x65, 0x61, 0x6C, 0x6F, 0xEE, 0xF2, 0xF6, 0xFC, 0x41, 0x62, 0xFC, 0x20, 0x21,
|
||||
0x69, 0xFC, 0x41, 0x63, 0xFC, 0x5F, 0x41, 0x61, 0xFC, 0x58, 0x22, 0x65, 0x74, 0xF8, 0xFC, 0x42, 0x67, 0x72, 0xFD,
|
||||
0xCE, 0xFC, 0x20, 0x41, 0x78, 0xFC, 0x05, 0x23, 0x65, 0x6F, 0x75, 0xF5, 0xFC, 0xE1, 0x41, 0x65, 0xFC, 0x95, 0x47,
|
||||
0x63, 0x65, 0x66, 0x68, 0x73, 0x74, 0x76, 0xFF, 0xA4, 0xFF, 0xB8, 0xFF, 0xCD, 0xFF, 0xDA, 0xFF, 0xE5, 0xFF, 0xF5,
|
||||
0xFF, 0xFC, 0x41, 0x6E, 0xFC, 0x31, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x47, 0x64, 0x65, 0x67, 0x6C, 0x6D, 0x6E,
|
||||
0x73, 0xFF, 0x30, 0xFF, 0x39, 0xFF, 0x47, 0xFF, 0x5F, 0xFF, 0x74, 0xFF, 0xE0, 0xFF, 0xFD, 0x43, 0x72, 0x73, 0x6E,
|
||||
0xFC, 0x69, 0xFC, 0x69, 0xFC, 0x69, 0x21, 0x61, 0xF6, 0x41, 0x6C, 0xFC, 0x04, 0x21, 0x6C, 0xFC, 0x41, 0x61, 0xFD,
|
||||
0xE7, 0x21, 0x74, 0xFC, 0xA0, 0x09, 0x53, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0x22, 0x73, 0x69, 0xF5, 0xFB, 0xC1, 0x05,
|
||||
0x92, 0x61, 0xFB, 0x98, 0x43, 0x6E, 0x72, 0x73, 0xFB, 0x92, 0xFF, 0xFA, 0xFB, 0x92, 0x43, 0x6E, 0x72, 0x73, 0xFB,
|
||||
0x88, 0xFB, 0x88, 0xFB, 0x88, 0x42, 0xA9, 0xB3, 0xFF, 0xF6, 0xFB, 0x7E, 0x45, 0x72, 0x64, 0x65, 0xC3, 0x6D, 0xFB,
|
||||
0x77, 0xFB, 0x77, 0xFF, 0xE5, 0xFF, 0xF9, 0xFB, 0x77, 0x42, 0x6E, 0x73, 0xFB, 0x67, 0xFB, 0x67, 0x42, 0xA1, 0xAD,
|
||||
0xFB, 0x60, 0xFF, 0xC8, 0x45, 0x69, 0x61, 0x65, 0x6F, 0xC3, 0xFF, 0xE2, 0xFF, 0xF2, 0xFB, 0x59, 0xFB, 0x59, 0xFF,
|
||||
0xF9, 0x41, 0xA1, 0xFB, 0x49, 0x42, 0x61, 0xC3, 0xFB, 0x45, 0xFF, 0xFC, 0x21, 0xB1, 0xF9, 0x41, 0x62, 0xFB, 0x9C,
|
||||
0x47, 0x64, 0x65, 0x62, 0x73, 0x6E, 0xC3, 0x72, 0xFF, 0x81, 0xFF, 0x88, 0xFF, 0x9A, 0xFF, 0x8F, 0xFF, 0xDE, 0xFF,
|
||||
0xF9, 0xFF, 0xFC, 0x46, 0xC3, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xFB, 0x5A, 0xFC, 0x32, 0xFD, 0x07, 0xFE, 0x4E, 0xFF,
|
||||
0x4B, 0xFF, 0xEA, 0x41, 0x69, 0xFB, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x45, 0x63, 0x6E, 0x72, 0x73, 0x69,
|
||||
0xFA, 0xCE, 0xF4, 0xA7, 0xFB, 0x01, 0xFF, 0xE3, 0xFF, 0xFD, 0xA0, 0x11, 0x72, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x22, 0x65, 0xC3, 0xFA, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x65, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72, 0x65, 0x69,
|
||||
0xE8, 0x5B, 0xE8, 0x5E, 0xE8, 0x64, 0xE8, 0x5E, 0xE8, 0x6D, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8,
|
||||
0x5E, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x77, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x80, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E,
|
||||
0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x8A, 0xFF, 0xDC, 0xFF, 0xFD, 0x42, 0x6D, 0x72, 0xF4, 0x38, 0xF3, 0xDE, 0x41, 0x69,
|
||||
0xF3, 0xD3, 0x43, 0x6C, 0x73, 0x74, 0xF9, 0xB2, 0xFF, 0xFC, 0xF9, 0xB2, 0x42, 0x6E, 0x74, 0xF9, 0xA8, 0xF9, 0xA8,
|
||||
0x41, 0x69, 0xEB, 0x9B, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x65, 0x69, 0x6F, 0x61, 0xE7, 0xDE, 0xE7, 0xE1, 0xE7, 0xE1,
|
||||
0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7,
|
||||
0xE1, 0xE7, 0xE1, 0xF7, 0xB7, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE8, 0x0D, 0xE8, 0x0D,
|
||||
0xFF, 0xCE, 0xFF, 0xD9, 0xFF, 0xE3, 0xFF, 0xFD, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0xE7, 0x8D, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7,
|
||||
0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xF7, 0x41, 0xA0, 0x08, 0xB1, 0x21, 0x2E, 0xFD, 0x49, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x2E,
|
||||
0x73, 0xE8, 0x4E, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x57, 0xFF, 0xFA, 0xFF, 0xFD,
|
||||
0x22, 0x2E, 0x73, 0xDE, 0xE1, 0x21, 0x61, 0xFB, 0x21, 0xAD, 0xFD, 0x23, 0x6F, 0x61, 0xC3, 0xD9, 0xF5, 0xFD, 0x21,
|
||||
0x66, 0xF9, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0xE7, 0x0E, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A,
|
||||
0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7,
|
||||
0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xFF, 0xFD, 0x41, 0x73,
|
||||
0xFF, 0x84, 0x42, 0x2E, 0x65, 0xFF, 0x7D, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x42, 0x6F, 0x61, 0xFF, 0x95, 0xFF, 0xFD,
|
||||
0x21, 0x6E, 0xF9, 0x41, 0x72, 0xF9, 0x23, 0x42, 0x65, 0x72, 0xFF, 0xFC, 0xE7, 0x37, 0x21, 0x74, 0xF9, 0x42, 0x6C,
|
||||
0x73, 0xF8, 0x4D, 0xFF, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE7, 0x64, 0xE7, 0x67, 0xE7, 0x67,
|
||||
0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x6D, 0x41, 0x6E, 0xF8, 0xFB, 0x21, 0x6F, 0xFC, 0x22, 0x6F, 0x72, 0xE3,
|
||||
0xFD, 0x41, 0x63, 0xE7, 0x04, 0x21, 0x65, 0xFC, 0x41, 0x61, 0xEA, 0x8B, 0x22, 0x6E, 0x67, 0xF9, 0xFC, 0x41, 0x64,
|
||||
0xF7, 0x46, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x6F, 0x61, 0x65, 0x69, 0x75,
|
||||
0xE6, 0x5D, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6,
|
||||
0x60, 0xF6, 0x36, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60,
|
||||
0xE6, 0x60, 0xFE, 0xD0, 0xFF, 0x4F, 0xFF, 0xAC, 0xFF, 0xBD, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x2E, 0xE6,
|
||||
0x0C, 0x42, 0x2E, 0x73, 0xE6, 0x08, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x6C, 0xFB, 0x42, 0x6F, 0x61,
|
||||
0xE6, 0xD5, 0xE6, 0xD5, 0x21, 0x6E, 0xF9, 0x21, 0x61, 0xFD, 0x22, 0x65, 0x6D, 0xF0, 0xFD, 0x41, 0x65, 0xFE, 0x9F,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFA, 0x22, 0x6C, 0x69, 0xFA, 0xFD, 0x42, 0x6C,
|
||||
0x62, 0xF7, 0x7C, 0xFF, 0xFB, 0x42, 0x63, 0x6F, 0xE6, 0x55, 0xE6, 0xEB, 0x21, 0x69, 0xF9, 0x41, 0x2E, 0xE8, 0xAA,
|
||||
0x42, 0x2E, 0x73, 0xE8, 0xA6, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x47, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xE9, 0x79, 0xE9, 0xB5, 0xE9, 0xB5, 0xE9, 0xB5, 0xFF, 0xFB, 0xE9, 0xB5, 0xE9, 0xBB, 0x43,
|
||||
0x61, 0x69, 0x6F, 0xF5, 0xB9, 0xFF, 0xEA, 0xE9, 0xB0, 0x42, 0x61, 0x74, 0xF5, 0xAF, 0xE6, 0xBD, 0x41, 0x72, 0xE6,
|
||||
0x11, 0x21, 0x65, 0xFC, 0x46, 0x63, 0x6C, 0x6D, 0x70, 0x74, 0x78, 0xF1, 0xA5, 0xFF, 0xBC, 0xFF, 0xE8, 0xFF, 0xF2,
|
||||
0xFF, 0xFD, 0xFF, 0x0D, 0xA0, 0x0A, 0x13, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD,
|
||||
0xA1, 0x06, 0xF3, 0x63, 0xFD, 0x21, 0x69, 0xEC, 0x21, 0x6D, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD,
|
||||
0x21, 0x61, 0xDE, 0x21, 0x69, 0xFD, 0xA2, 0x06, 0xF3, 0x6E, 0x78, 0xF5, 0xFD, 0xA0, 0x0A, 0x43, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x07, 0x23, 0x6E, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF6, 0xA6,
|
||||
0xF6, 0xA6, 0xF6, 0xA6, 0xFF, 0xFB, 0xF6, 0xA6, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE9, 0xF3,
|
||||
0xE9, 0xCA, 0xF6, 0x93, 0xF6, 0x93, 0xFF, 0xBF, 0xFF, 0xD8, 0xF6, 0x93, 0xFF, 0xF0, 0x21, 0x72, 0xE7, 0x42, 0x65,
|
||||
0x6F, 0xFF, 0xFD, 0xE9, 0x19, 0x43, 0x64, 0x70, 0x73, 0xF0, 0xC5, 0xFF, 0xF9, 0xF1, 0x1F, 0x42, 0x6F, 0x65, 0xE9,
|
||||
0x08, 0xF0, 0xB7, 0x42, 0x6C, 0x6D, 0xF6, 0x93, 0xFF, 0xF9, 0x5B, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0x61, 0x65, 0x69, 0x6F,
|
||||
0xE4, 0xDF, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4,
|
||||
0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2,
|
||||
0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xFE, 0xF6, 0xFF, 0x10, 0xFF, 0x62, 0xFF, 0xE8, 0xFF, 0xF9, 0xD6, 0x00, 0x41,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xE4, 0x8D, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90,
|
||||
0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4,
|
||||
0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0x41, 0x6C, 0xF5, 0xF5, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72,
|
||||
0x69, 0xE4, 0x44, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47,
|
||||
0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4,
|
||||
0x47, 0xE4, 0x47, 0xE4, 0x73, 0xE4, 0x73, 0xFF, 0xFC, 0xD6, 0x00, 0x81, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xE3, 0xFC, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3,
|
||||
0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0x41, 0x75, 0xF3, 0x6B, 0x41, 0x66, 0xEF, 0x7D, 0xA0, 0x0D, 0x02, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0xA1, 0xFD, 0x44, 0x6E, 0x70, 0x74, 0xC3, 0xFF, 0xED, 0xF5, 0x4D, 0xF5, 0x4D, 0xFF, 0xFD,
|
||||
0x41, 0x61, 0xFC, 0x4E, 0x21, 0xAD, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C,
|
||||
0x65, 0x69, 0x6F, 0xE3, 0x86, 0xE3, 0x89, 0xE3, 0x8F, 0xE3, 0x89, 0xE3, 0x98, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0x89,
|
||||
0xE3, 0x89, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0xA2, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0xAB, 0xE3, 0x89, 0xE3,
|
||||
0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xFF, 0x8A, 0xFF, 0xCF, 0xFF, 0xE6, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xE3,
|
||||
0x38, 0xF4, 0x32, 0x21, 0x65, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x62, 0xFD, 0x41, 0x2E, 0xE4, 0x07, 0x21, 0x65, 0xFC,
|
||||
0x21, 0x74, 0xFD, 0x48, 0x6C, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xAB, 0xE6, 0xEC, 0xE7, 0x28, 0xE7,
|
||||
0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x2E, 0x21, 0x61, 0xE7, 0x41, 0x6E, 0xE3, 0x8F, 0x21, 0x61, 0xFC,
|
||||
0x47, 0x6F, 0x61, 0x6E, 0x67, 0x6C, 0x73, 0x74, 0xF3, 0xF5, 0xFF, 0xD0, 0xFF, 0xDA, 0xFF, 0xF6, 0xFF, 0xFD, 0xF4,
|
||||
0xA8, 0xFC, 0x8B, 0xA0, 0x05, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA0, 0x02, 0xB2, 0xCC,
|
||||
0x01, 0xA1, 0x68, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x70, 0x71, 0x73, 0x74, 0x76, 0xFF, 0xFD, 0xE4, 0xE9, 0xE4,
|
||||
0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9,
|
||||
0x41, 0x69, 0xE6, 0xCA, 0x44, 0x6E, 0x63, 0x6C, 0x78, 0xFF, 0xCF, 0xEE, 0x79, 0xFF, 0xD5, 0xFF, 0xFC, 0x41, 0x72,
|
||||
0xE8, 0xA2, 0x21, 0x61, 0xFC, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x74, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE6, 0x54, 0xFF, 0xFB, 0xE6, 0x90, 0xE6, 0x90,
|
||||
0xE6, 0x90, 0xE6, 0x90, 0xE6, 0x96, 0x21, 0x69, 0xEA, 0x41, 0x69, 0xE3, 0x9F, 0x44, 0x63, 0x6C, 0x6E, 0x72, 0xEE,
|
||||
0x37, 0xFF, 0xD2, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xE6, 0x62, 0x21, 0x6C, 0xFC, 0x43, 0x6E, 0x72, 0x74, 0xF4,
|
||||
0x02, 0xFE, 0xA2, 0xF4, 0x02, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x61, 0x69, 0x75, 0x6F, 0xE2, 0x4B, 0xE2,
|
||||
0x4E, 0xE2, 0x54, 0xE2, 0x4E, 0xE2, 0x5D, 0xE2, 0x62, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x62,
|
||||
0xF2, 0x24, 0xE2, 0x67, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x70, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2,
|
||||
0x4E, 0xE2, 0x4E, 0xFF, 0x50, 0xFF, 0xA0, 0xFF, 0xE2, 0xFF, 0xF3, 0xFF, 0xF6, 0xA0, 0x0B, 0x95, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0xC3, 0x00, 0x71, 0x7A, 0x73, 0x65, 0xE1, 0xF1, 0xE1, 0xF1, 0xFF, 0xFD, 0x41,
|
||||
0x74, 0xED, 0xA2, 0x42, 0x2E, 0x72, 0xE1, 0xDE, 0xFF, 0xFC, 0x43, 0x6D, 0x6E, 0x72, 0xF3, 0x81, 0xF3, 0x81, 0xF1,
|
||||
0x88, 0x45, 0x63, 0x66, 0x6F, 0x74, 0x75, 0xED, 0x98, 0xED, 0x98, 0xFB, 0x31, 0xF3, 0x77, 0xF2, 0xA5, 0xD9, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x6F, 0x61, 0x65, 0xE1, 0xBA, 0xE1, 0xBD, 0xE1, 0xC3, 0xE1, 0xBD, 0xE1, 0xCC, 0xE1, 0xD1,
|
||||
0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xD1, 0xE1, 0xBD, 0xE1, 0xD6, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1,
|
||||
0xBD, 0xFF, 0xCF, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xFF, 0xDF, 0xFF, 0xE6, 0xFF, 0xF0,
|
||||
0xC1, 0x0D, 0x22, 0x6F, 0xE2, 0x8F, 0x42, 0x63, 0x71, 0xFF, 0xFA, 0xF1, 0x1E, 0xC2, 0x00, 0x71, 0x2E, 0x69, 0xE1,
|
||||
0x5F, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x2E, 0x65, 0xE1, 0x56, 0xED, 0x24, 0x41, 0x74, 0xFE, 0xB9, 0x21, 0x63, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x41, 0x72, 0xE5, 0x49, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x75, 0xE1, 0x3F, 0xE1, 0x6B,
|
||||
0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1,
|
||||
0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B,
|
||||
0xE1, 0x6B, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x70, 0xE2, 0xB2, 0x42, 0x6D, 0x74, 0xFF, 0xFC, 0xEC, 0xBA, 0xD7, 0x00,
|
||||
0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x6F, 0xE0, 0xE9, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15,
|
||||
0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1,
|
||||
0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xFF, 0xF9, 0x42, 0x61, 0x6F, 0xF1, 0x95, 0xF1,
|
||||
0x95, 0x21, 0x74, 0xF9, 0x41, 0x61, 0xF4, 0x4F, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0xA0, 0x10, 0x92, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x10, 0xB2, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x23, 0x65, 0x61, 0x70, 0xE2, 0xEE, 0xFD, 0x44, 0x64, 0x72, 0x6E, 0x74, 0xF1, 0x7E, 0xFF, 0xF9,
|
||||
0xF1, 0x42, 0xF9, 0xFB, 0x41, 0x6E, 0xEB, 0x6F, 0x21, 0x6F, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x65,
|
||||
0xEC, 0x1B, 0xA0, 0x06, 0x31, 0x41, 0xB1, 0xE1, 0xF2, 0x21, 0xC3, 0xFC, 0x22, 0x2E, 0x65, 0xF6, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x73, 0xFB, 0x41, 0x61, 0xE6, 0x3D, 0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x41,
|
||||
0x6F, 0xE6, 0x2E, 0xA1, 0x04, 0xC2, 0x73, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xE4, 0x2E, 0xE4, 0x2E, 0xFF,
|
||||
0xFB, 0xE4, 0x2E, 0xE4, 0x2E, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xDF, 0xE4, 0x1B, 0xE4, 0x1B,
|
||||
0xFF, 0xD3, 0xE4, 0x1B, 0xFF, 0xE2, 0xFF, 0xF0, 0x21, 0x61, 0xEA, 0x23, 0x6E, 0x6C, 0x72, 0xA4, 0xA7, 0xFD, 0x41,
|
||||
0x7A, 0xEB, 0xBB, 0x43, 0x63, 0x65, 0x72, 0xF1, 0x9A, 0xFF, 0xFC, 0xF1, 0x9A, 0x42, 0x71, 0x63, 0xE5, 0xE7, 0xFF,
|
||||
0xAA, 0x41, 0x65, 0xFF, 0xA3, 0x42, 0x64, 0x74, 0xFD, 0x3A, 0xFF, 0xFC, 0xA2, 0x04, 0xA2, 0x72, 0x6E, 0xEE, 0xF9,
|
||||
0x41, 0x65, 0xFD, 0x36, 0x21, 0x69, 0xFC, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0xC1, 0x04, 0xA2, 0x72, 0xE1, 0x66, 0x41,
|
||||
0x71, 0xE5, 0xBC, 0xA1, 0x04, 0xC2, 0x72, 0xFC, 0x41, 0x65, 0xE5, 0xB3, 0x21, 0x74, 0xFC, 0xA1, 0x04, 0xC2, 0x73,
|
||||
0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xEF, 0xFF, 0xFB, 0xE3, 0xB0, 0xE3, 0xB0, 0xE3, 0xB0, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0x61, 0xFF, 0xC2, 0xE3, 0x9D, 0xE3, 0x9D, 0xFF, 0xD0, 0xFF, 0xD5, 0xFF,
|
||||
0xF0, 0x21, 0x69, 0xEA, 0x41, 0x6F, 0xEA, 0x8B, 0xA1, 0x04, 0x52, 0x72, 0xFC, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xE0, 0x80, 0xE0, 0x83, 0xFF, 0xFB, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x89, 0x21, 0x61, 0xEA,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x72, 0xFC, 0x7C, 0x21, 0x70, 0xFC, 0x41, 0x64, 0xFC, 0x75, 0x22, 0x6D, 0x6E, 0xF9, 0xFC,
|
||||
0xA0, 0x0E, 0x13, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x0E, 0x43, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xD8, 0x22, 0x6C, 0x73, 0xFA, 0xFD, 0x41, 0x2E, 0xE1, 0x38, 0x42, 0x2E,
|
||||
0x73, 0xE1, 0x34, 0xFF, 0xFC, 0x42, 0x61, 0x73, 0xFF, 0xF9, 0xE1, 0xD0, 0x24, 0x69, 0x6F, 0x65, 0x74, 0xC9, 0xD7,
|
||||
0xE9, 0xF9, 0x43, 0x6C, 0x72, 0x73, 0xFF, 0x8D, 0xFF, 0xB2, 0xFF, 0xF7, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x75,
|
||||
0x65, 0x61, 0x69, 0x6F, 0xDF, 0x00, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF,
|
||||
0x03, 0xDF, 0x03, 0xDF, 0x03, 0xEE, 0xD9, 0xDF, 0x03, 0xDF, 0x03, 0xFD, 0xA1, 0xFD, 0xAA, 0xDF, 0x03, 0xDF, 0x03,
|
||||
0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xFD, 0xC1, 0xFE, 0x17, 0xFE, 0x66, 0xFE, 0x95, 0xFF, 0x08, 0xFF, 0x13, 0xFF,
|
||||
0xF6, 0x42, 0x6D, 0x72, 0xDF, 0x3C, 0xEA, 0x76, 0x42, 0x65, 0x69, 0xFC, 0xC6, 0xFF, 0xF9, 0xD7, 0x00, 0x41, 0x2E,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x75, 0xDE, 0x9E, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1,
|
||||
0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE,
|
||||
0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x6E, 0x61, 0xDE, 0x5C, 0xEE,
|
||||
0xD0, 0x41, 0xA1, 0xF0, 0xDB, 0x43, 0x61, 0xC3, 0x65, 0xF0, 0xD7, 0xFF, 0xFC, 0xF0, 0xD7, 0x21, 0x69, 0xF6, 0x21,
|
||||
0x63, 0xFD, 0xA0, 0x0A, 0x72, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69,
|
||||
0xC3, 0xEE, 0xFD, 0x21, 0x6E, 0xFB, 0x42, 0x65, 0x72, 0xE2, 0x3D, 0xE9, 0xEC, 0x22, 0x69, 0x74, 0xF6, 0xF9, 0xA0,
|
||||
0x0B, 0xB1, 0x23, 0xA1, 0xA9, 0xAD, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0xC3, 0x65, 0x6F, 0xF6, 0xF9, 0xF6, 0xF6, 0x43,
|
||||
0x64, 0x6E, 0x72, 0xF5, 0xFA, 0xED, 0xB7, 0xFF, 0xF7, 0x41, 0x6D, 0xEF, 0xA6, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72,
|
||||
0x65, 0x61, 0x6F, 0xDD, 0xF5, 0xDD, 0xF8, 0xDD, 0xFE, 0xDD, 0xF8, 0xDE, 0x07, 0xDE, 0x0C, 0xDD, 0xF8, 0xDD, 0xF8,
|
||||
0xDD, 0xF8, 0xDD, 0xF8, 0xFF, 0x9F, 0xDD, 0xF8, 0xDE, 0x11, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x1A, 0xDD, 0xF8, 0xDD,
|
||||
0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x24, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xFC, 0xC4, 0x00, 0x71, 0x74,
|
||||
0x73, 0x6E, 0x61, 0xDD, 0xAD, 0xDD, 0xAD, 0xDD, 0xAD, 0xEE, 0x21, 0xA0, 0x00, 0xD1, 0x21, 0x2E, 0xFD, 0x22, 0x2E,
|
||||
0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x02, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x2E, 0x65, 0xEC, 0xFD, 0x21, 0x6C,
|
||||
0xFB, 0x22, 0x2E, 0x73, 0xEF, 0xF2, 0x21, 0x6E, 0xED, 0x21, 0xB3, 0xFD, 0x21, 0x65, 0xEA, 0x21, 0x6E, 0xFD, 0x23,
|
||||
0x61, 0xC3, 0x6F, 0xEF, 0xF7, 0xFD, 0x21, 0x6C, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xC9, 0x23, 0x2E, 0x61, 0x65,
|
||||
0xC3, 0xC9, 0xFD, 0x21, 0x72, 0xF9, 0xC6, 0x00, 0x71, 0x7A, 0x73, 0x65, 0x61, 0x69, 0x6F, 0xDD, 0x57, 0xDD, 0x57,
|
||||
0xFF, 0xBF, 0xFF, 0xD2, 0xFF, 0xF0, 0xFF, 0xFD, 0x41, 0x74, 0xDF, 0xF2, 0x21, 0x63, 0xFC, 0x41, 0x76, 0xDE, 0x67,
|
||||
0x44, 0x6E, 0x2E, 0x73, 0x6C, 0xFF, 0xF9, 0xF5, 0xEC, 0xF5, 0xEF, 0xFF, 0xFC, 0x41, 0x65, 0xFA, 0x22, 0x41, 0x76,
|
||||
0xE8, 0xEA, 0xA0, 0x0E, 0xD2, 0xA0, 0x0E, 0xF3, 0xA0, 0x0F, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21,
|
||||
0x6F, 0xF1, 0x21, 0x64, 0xFD, 0x44, 0x6C, 0x6D, 0x72, 0x75, 0xFF, 0xCF, 0xFA, 0x44, 0xFF, 0xD3, 0xFF, 0xFD, 0xA0,
|
||||
0x0F, 0x52, 0xA1, 0x0F, 0x52, 0x73, 0xFD, 0x21, 0x61, 0xFB, 0xA1, 0x04, 0x52, 0x73, 0xFD, 0x47, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xDD, 0xE5, 0xFF, 0xFB, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xEE, 0x21,
|
||||
0x65, 0xEA, 0x21, 0x72, 0xFD, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF4, 0xB1, 0xA0, 0x0F, 0xF3, 0xA1, 0x06, 0xF3, 0x72,
|
||||
0xFD, 0x41, 0x72, 0xF9, 0xC6, 0xA1, 0x06, 0xF3, 0x6F, 0xFC, 0xA0, 0x10, 0x23, 0x41, 0x2E, 0xF7, 0x64, 0x42, 0x2E,
|
||||
0x73, 0xF7, 0x60, 0xFF, 0xFC, 0x21, 0x74, 0xF9, 0x21, 0x69, 0xFD, 0xA2, 0x07, 0x23, 0x72, 0x76, 0xEC, 0xFD, 0x45,
|
||||
0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xF9, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03, 0x48, 0x72, 0x68, 0x61,
|
||||
0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE1, 0x50, 0xE1, 0x27, 0xFF, 0xC7, 0xED, 0xF0, 0xFF, 0xD0, 0xED, 0xF0, 0xED, 0xF0,
|
||||
0xFF, 0xF0, 0x21, 0x72, 0xE7, 0xC7, 0x07, 0xB1, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xDD, 0x6A, 0xDD, 0x6D,
|
||||
0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x73, 0x21, 0x61, 0xE8, 0x22, 0x65, 0x72, 0xE2, 0xFD, 0xA0,
|
||||
0x11, 0x43, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x70, 0x64, 0x72, 0xE0, 0xFD, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7A, 0x61, 0x65, 0x6F, 0x75, 0xDC, 0x19, 0xDC, 0x1C, 0xDC, 0x22, 0xDC, 0x1C, 0xDC, 0x2B, 0xDC, 0x30, 0xDC, 0x1C,
|
||||
0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x30, 0xDC, 0x1C, 0xFE, 0x72, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE,
|
||||
0xC8, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE, 0xE8, 0xFF, 0x26, 0xFF, 0x5F, 0xFF, 0xF9,
|
||||
0x41, 0x65, 0xE1, 0x23, 0x41, 0x6E, 0xDF, 0x92, 0x21, 0x69, 0xFC, 0x22, 0x74, 0x64, 0xF5, 0xFD, 0x41, 0x6C, 0xDF,
|
||||
0x86, 0x21, 0x65, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x62, 0xDF, 0x7C, 0x21, 0x6F, 0xFC, 0x41, 0x72, 0xDF, 0x75, 0x21,
|
||||
0x61, 0xFC, 0x43, 0x63, 0x70, 0x74, 0xFF, 0xF6, 0xDF, 0x6E, 0xFF, 0xFD, 0x41, 0xA1, 0xDF, 0x8F, 0x21, 0xC3, 0xFC,
|
||||
0x21, 0x6C, 0xFD, 0x24, 0x6E, 0x62, 0x6C, 0x74, 0xCF, 0xDB, 0xEC, 0xFD, 0x21, 0xA1, 0xBF, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x2E, 0xE4, 0xB3, 0x42, 0x2E, 0x73, 0xE4, 0xAF, 0xFF, 0xFC, 0x22, 0x6F, 0x61,
|
||||
0xF9, 0xF9, 0x21, 0x72, 0xFB, 0x23, 0x61, 0x6F, 0x65, 0xD8, 0xEA, 0xFD, 0x41, 0x73, 0xDE, 0x49, 0x21, 0x61, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x64, 0xE0, 0x00, 0x41, 0x6C, 0xDF, 0xFC, 0x41, 0x69, 0xE9, 0x65, 0x42,
|
||||
0x63, 0x74, 0xDF, 0xF7, 0xFF, 0xFC, 0xA4, 0x0E, 0xB2, 0x6D, 0x6E, 0x74, 0x63, 0xEA, 0xED, 0xF1, 0xF9, 0x41, 0xBA,
|
||||
0xE4, 0x87, 0x41, 0x75, 0xDF, 0xE5, 0xA2, 0x0E, 0xB2, 0xC3, 0x78, 0xF8, 0xFC, 0x41, 0x6E, 0xDF, 0xDA, 0x21, 0x61,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0xB3, 0xF0, 0x22, 0x6F, 0xC3, 0xED, 0xFD, 0x21,
|
||||
0x69, 0xFB, 0x41, 0x2E, 0xDB, 0x9E, 0x42, 0x2E, 0x73, 0xDB, 0x9A, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x41,
|
||||
0xAD, 0xDF, 0xAF, 0x43, 0x69, 0xC3, 0x65, 0xDF, 0xAB, 0xFF, 0xFC, 0xDF, 0xAB, 0x41, 0xA1, 0xDF, 0xA1, 0x43, 0x61,
|
||||
0xC3, 0x6F, 0xDF, 0x9D, 0xFF, 0xFC, 0xDF, 0x9D, 0x41, 0x61, 0xE4, 0x31, 0x21, 0x76, 0xFC, 0x41, 0x74, 0xDD, 0x80,
|
||||
0x41, 0x69, 0xDF, 0x88, 0xA1, 0x0E, 0x92, 0x72, 0xFC, 0x41, 0x76, 0xDF, 0x7F, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x69,
|
||||
0xDF, 0x7B, 0xDF, 0xF0, 0xDF, 0x7B, 0xFF, 0xF7, 0xFF, 0xFC, 0xC8, 0x0E, 0xB2, 0x62, 0x63, 0x64, 0x67, 0x6A, 0x6C,
|
||||
0x73, 0x74, 0xFF, 0x9E, 0xFF, 0xA9, 0xFF, 0xB7, 0xFF, 0xC0, 0xFF, 0xCE, 0xFF, 0xDC, 0xFF, 0xDF, 0xFF, 0xF0, 0x41,
|
||||
0x65, 0xDF, 0x49, 0x41, 0x69, 0xDD, 0x81, 0x21, 0x63, 0xFC, 0x21, 0x61, 0xFD, 0xA2, 0x0E, 0xB2, 0x63, 0x72, 0xF2,
|
||||
0xFD, 0xC3, 0x0E, 0xB2, 0x72, 0x62, 0x73, 0xDF, 0x34, 0xE1, 0xB9, 0xE0, 0xFB, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xDF, 0x28, 0xFF, 0x3B, 0xFF, 0x4E, 0xFF, 0xC4, 0xFF, 0xED, 0xFF, 0xF4, 0xE5, 0xE3, 0x21, 0x73, 0xEA,
|
||||
0x42, 0x73, 0x6E, 0xFE, 0xFB, 0xFF, 0xFD, 0x41, 0x70, 0xE6, 0x22, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x6F,
|
||||
0xDA, 0x54, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA,
|
||||
0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80,
|
||||
0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xFF, 0xF5, 0xFF, 0xFC, 0x41, 0x68, 0xEC, 0xA2, 0x21, 0x63, 0xFC, 0xC2, 0x01,
|
||||
0xF2, 0x2E, 0x73, 0xDA, 0x02, 0xFF, 0xFD, 0xC1, 0x01, 0xF2, 0x2E, 0xD9, 0xF9, 0xA0, 0x01, 0xF2, 0x42, 0x61, 0x72,
|
||||
0xF6, 0xB8, 0xDB, 0x22, 0x41, 0x65, 0xDE, 0x04, 0x42, 0x61, 0x6D, 0xDE, 0x00, 0xE5, 0xAF, 0x44, 0x74, 0x6C, 0x63,
|
||||
0x72, 0xFF, 0xEE, 0xFF, 0xF5, 0xEA, 0x58, 0xFF, 0xF9, 0x41, 0x75, 0xEC, 0x56, 0x41, 0x6F, 0xEC, 0x52, 0x22, 0x71,
|
||||
0x63, 0xF8, 0xFC, 0x21, 0x6F, 0xFB, 0x41, 0x6C, 0xEA, 0x9C, 0x41, 0x70, 0xEB, 0x6A, 0x41, 0x62, 0xE5, 0x83, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x63, 0xDA, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0xDC,
|
||||
0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x74, 0x76, 0x77, 0x79,
|
||||
0x72, 0x7A, 0x73, 0x6C, 0x78, 0x65, 0x69, 0x61, 0x6F, 0x75, 0xC3, 0xD9, 0xA2, 0xD9, 0xA5, 0xD9, 0xAB, 0xD9, 0xA5,
|
||||
0xD9, 0xB4, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xBE, 0xD9, 0xA5, 0xD9,
|
||||
0xC7, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xFF, 0x4E, 0xFF, 0xA0, 0xFF, 0xA9, 0xFF, 0xAF, 0xFF, 0xAF, 0xFF, 0xC4,
|
||||
0xFF, 0xDE, 0xFF, 0xE1, 0xFF, 0xE5, 0xFF, 0xED, 0xFF, 0xFD, 0x42, 0x63, 0x64, 0xFF, 0x62, 0xF8, 0xFA, 0xD7, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x6C, 0x72, 0x69, 0xD9, 0x44, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47,
|
||||
0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9,
|
||||
0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x73, 0xD9, 0x73, 0xFF, 0xF9, 0x41, 0x73, 0xFE, 0xF3, 0xD7, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x61, 0xD8, 0xF8, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB,
|
||||
0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8,
|
||||
0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xFF, 0xFC, 0x42, 0x6E, 0x72, 0xEA, 0x5D, 0xEA,
|
||||
0x5D, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x69, 0xD8, 0xA9, 0xD8, 0xAC, 0xD8, 0xB2, 0xD8, 0xAC, 0xD8, 0xBB,
|
||||
0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xC5, 0xD8, 0xAC, 0xD8,
|
||||
0xAC, 0xD8, 0xAC, 0xD8, 0xCE, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xFF, 0xF9, 0xF4, 0x61,
|
||||
0xD6, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xD8, 0x5E, 0xD8, 0x61, 0xD8, 0x67, 0xD8, 0x61, 0xD8, 0x70, 0xD8, 0x75, 0xD8,
|
||||
0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x75, 0xD8, 0x61, 0xD8, 0x7A, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61,
|
||||
0xD8, 0x83, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0x41, 0x6F, 0xF1, 0x80, 0xD7, 0x00, 0x41,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x6F, 0xD8, 0x15, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8,
|
||||
0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18,
|
||||
0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xFF, 0xFC, 0xC1, 0x00, 0x41, 0x2E, 0xD7, 0xCD, 0x41,
|
||||
0x73, 0xE8, 0xC1, 0xA0, 0x02, 0x82, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4,
|
||||
0x80, 0xF4, 0x80, 0xFF, 0xFB, 0x21, 0x6C, 0xF6, 0x21, 0x65, 0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4, 0x70, 0xF4, 0x70,
|
||||
0xF4, 0x70, 0x21, 0x6C, 0xF6, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0xA0, 0x02, 0xD2, 0x21, 0x2E,
|
||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x22, 0x6F, 0x61, 0xFB, 0xFB, 0x21, 0x63, 0xFB, 0x21, 0x69, 0xFD, 0x25, 0x6D,
|
||||
0x74, 0x73, 0x6E, 0x72, 0xD1, 0xD1, 0xE1, 0xE7, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xE5, 0xE5, 0xE5, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x73, 0xFD, 0x25, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xB9, 0xB9, 0xC9, 0xCF, 0xFD, 0x46, 0x73, 0x69, 0x2E, 0x64,
|
||||
0x6F, 0x72, 0xD7, 0x59, 0xFF, 0x92, 0xD7, 0x59, 0xFF, 0xDD, 0xFF, 0xC1, 0xFF, 0xF5, 0x41, 0x73, 0xFF, 0x86, 0x21,
|
||||
0x6F, 0xFC, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0x3F, 0xE8, 0x39, 0xFF, 0xFD, 0xFF, 0x78, 0xE8, 0x39, 0xA0,
|
||||
0x02, 0xA3, 0x21, 0x2E, 0xFD, 0x23, 0x2E, 0x73, 0x69, 0xFA, 0xFD, 0xE3, 0x42, 0x6F, 0x61, 0xF3, 0xEA, 0xF3, 0xEA,
|
||||
0x21, 0x63, 0xF9, 0x43, 0x65, 0x61, 0x69, 0xFF, 0xEF, 0xF3, 0xE0, 0xFF, 0xFD, 0x41, 0x6F, 0xF3, 0xD6, 0x22, 0x74,
|
||||
0x6D, 0xF2, 0xFC, 0x41, 0x73, 0xFF, 0x76, 0x21, 0x65, 0xFC, 0x21, 0x6F, 0xF9, 0x41, 0x65, 0xFF, 0x6F, 0x21, 0x6C,
|
||||
0xFC, 0x47, 0x61, 0x2E, 0x73, 0x74, 0x6D, 0x64, 0x62, 0xFF, 0xB5, 0xD6, 0xF4, 0xFF, 0xEA, 0xFF, 0xF3, 0xFF, 0xF6,
|
||||
0xFF, 0x6D, 0xFF, 0xFD, 0x21, 0x6D, 0xE0, 0x42, 0x2E, 0x65, 0xD6, 0xDB, 0xFF, 0xFD, 0x21, 0x61, 0xF6, 0xA0, 0x02,
|
||||
0xA2, 0x42, 0x2E, 0x73, 0xFF, 0xFD, 0xFF, 0xA2, 0x42, 0x2E, 0x73, 0xFF, 0x98, 0xFF, 0x9B, 0x23, 0x65, 0x6F, 0x61,
|
||||
0xF2, 0xF2, 0xF9, 0x21, 0x6C, 0xF9, 0x21, 0x65, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xEC, 0xEC, 0xEC, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0x47, 0x61, 0x65, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xFF, 0xC2,
|
||||
0xFF, 0xC2, 0xFF, 0xEA, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xFD, 0xFF, 0x08, 0x44, 0x6D, 0x74, 0x73, 0x6E, 0xFE, 0xDF,
|
||||
0xFE, 0xDF, 0xFE, 0xEF, 0xFE, 0xF5, 0x41, 0x6F, 0xFE, 0xB6, 0x43, 0x6F, 0x61, 0x65, 0xF3, 0x41, 0xF3, 0x41, 0xF3,
|
||||
0x41, 0x42, 0x2E, 0x6C, 0xD6, 0x6F, 0xFF, 0xF6, 0x21, 0x65, 0xF9, 0x41, 0x65, 0xE7, 0x5F, 0x44, 0x2E, 0x6D, 0x6C,
|
||||
0x6E, 0xD6, 0x61, 0xFF, 0xFC, 0xFF, 0xE8, 0xFF, 0xE4, 0x21, 0x65, 0xF3, 0x46, 0x6C, 0x6E, 0x6F, 0x6D, 0x74, 0x73,
|
||||
0xFE, 0xA9, 0xFF, 0xD4, 0xFE, 0x8A, 0xFF, 0xE9, 0xFF, 0xFD, 0xFF, 0xFD, 0x21, 0x6F, 0xED, 0x21, 0x64, 0xFD, 0x47,
|
||||
0x73, 0x69, 0x62, 0x72, 0x64, 0x6F, 0x6E, 0xFF, 0x5D, 0xFE, 0x71, 0xFF, 0x64, 0xFF, 0x98, 0xFF, 0xAE, 0xFE, 0xA0,
|
||||
0xFF, 0xFD, 0x41, 0x67, 0xFE, 0x9B, 0x21, 0x6F, 0xFC, 0x41, 0x63, 0xD6, 0x1E, 0x21, 0x69, 0xFC, 0x41, 0x65, 0xFF,
|
||||
0x06, 0x21, 0x74, 0xFC, 0x45, 0x2E, 0x6C, 0x74, 0x6E, 0x73, 0xD6, 0x0D, 0xFF, 0xEF, 0xFF, 0xF6, 0xE7, 0x07, 0xFF,
|
||||
0xFD, 0x45, 0xB1, 0xA9, 0xAD, 0xA1, 0xB3, 0xFE, 0x30, 0xFE, 0xA4, 0xFF, 0x09, 0xFF, 0xC5, 0xFF, 0xF0, 0xA0, 0x01,
|
||||
0x72, 0xA0, 0x01, 0x92, 0x21, 0xB3, 0xFD, 0x22, 0x75, 0xC3, 0xF7, 0xFD, 0x21, 0x65, 0xF2, 0xA0, 0x02, 0x62, 0x21,
|
||||
0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x65, 0xF5, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xE6, 0xE9, 0xFD, 0x22, 0x6F, 0x61, 0xE5, 0xF9, 0x21, 0x63, 0xFB, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0x61, 0xD4, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67,
|
||||
0xFD, 0x41, 0x67, 0xE1, 0x68, 0x23, 0xC3, 0x6F, 0x69, 0xED, 0xF9, 0xFC, 0xA0, 0x00, 0xC2, 0x21, 0x2E, 0xFD, 0x22,
|
||||
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x65, 0xF8, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73,
|
||||
0x6D, 0xE9, 0xEC, 0xFD, 0x44, 0x2E, 0x6F, 0x61, 0x74, 0xD5, 0x78, 0xFF, 0xE8, 0xFF, 0xF9, 0xF5, 0x24, 0x42, 0x6F,
|
||||
0x61, 0xD9, 0x83, 0xD9, 0x83, 0x21, 0x74, 0xF9, 0x41, 0x6E, 0xF2, 0xAF, 0x43, 0x63, 0x74, 0x65, 0xE7, 0x07, 0xE7,
|
||||
0x07, 0xFD, 0x93, 0x41, 0x74, 0xE6, 0xFD, 0x41, 0x69, 0xE5, 0x70, 0x41, 0x61, 0xD6, 0xF0, 0x21, 0xAD, 0xFC, 0x21,
|
||||
0xC3, 0xFD, 0xA1, 0x04, 0xA2, 0x70, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xD9, 0x07, 0xD9, 0x43,
|
||||
0xFF, 0xFB, 0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x49, 0x21, 0x6F, 0xEA, 0x22, 0x6E, 0x74, 0xD4, 0xFD, 0xA0,
|
||||
0x00, 0x91, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x0F, 0x72, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD,
|
||||
0x22, 0x6F, 0x61, 0xFB, 0xFB, 0xA0, 0x03, 0x32, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x10, 0xF3,
|
||||
0x21, 0x2E, 0xFD, 0x23, 0x61, 0x2E, 0x73, 0xF5, 0xFA, 0xFD, 0x21, 0x73, 0xEB, 0x22, 0x2E, 0x65, 0xE5, 0xFD, 0x21,
|
||||
0x6C, 0xFB, 0x22, 0x65, 0x61, 0xEE, 0xFD, 0x22, 0x63, 0x64, 0xD3, 0xFB, 0x4D, 0x65, 0x61, 0x2E, 0x78, 0x6C, 0x73,
|
||||
0x63, 0x6D, 0x6E, 0x70, 0x72, 0x6F, 0x69, 0xFE, 0xF1, 0xFE, 0xF6, 0xD4, 0xD5, 0xFF, 0x04, 0xFF, 0x3B, 0xFF, 0x60,
|
||||
0xFF, 0x74, 0xFF, 0x77, 0xFF, 0x7B, 0xFF, 0x85, 0xFF, 0xB5, 0xFF, 0xC0, 0xFF, 0xFB, 0x42, 0x65, 0xC3, 0xFE, 0xC0,
|
||||
0xFE, 0xC6, 0xA0, 0x03, 0x23, 0x21, 0x2E, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x43, 0x2E,
|
||||
0x73, 0x69, 0xD4, 0x97, 0xE5, 0x91, 0xFC, 0xD0, 0x21, 0x65, 0xF6, 0x41, 0x73, 0xFE, 0xB1, 0x44, 0x2E, 0x73, 0x69,
|
||||
0x6E, 0xFE, 0xAA, 0xFE, 0xAD, 0xFF, 0xFC, 0xFE, 0xAD, 0x43, 0x2E, 0x74, 0x65, 0xD4, 0x79, 0xFF, 0xEC, 0xFF, 0xF3,
|
||||
0xA0, 0x0C, 0xF1, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x11, 0x22, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0x6F, 0x69, 0x65, 0xC7, 0xEB, 0xFD, 0x42,
|
||||
0x6F, 0x72, 0xD4, 0x4A, 0xE0, 0x14, 0x45, 0x2E, 0x64, 0x66, 0x67, 0x74, 0xD4, 0x43, 0xFF, 0xF9, 0xF1, 0x94, 0xE5,
|
||||
0xEC, 0xFA, 0x5A, 0x41, 0x65, 0xFC, 0x6C, 0x42, 0x6E, 0x73, 0xFE, 0x56, 0xFE, 0x56, 0x41, 0x6F, 0xFF, 0x9E, 0x41,
|
||||
0x73, 0xFE, 0x48, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFE, 0x44, 0xFE, 0x47, 0xFF, 0xF8, 0xFF, 0xFC, 0xFE, 0x47,
|
||||
0x42, 0x61, 0x73, 0xFF, 0xF0, 0xFE, 0x37, 0x43, 0x2E, 0x73, 0x69, 0xFE, 0x2D, 0xFE, 0x30, 0xFF, 0x7F, 0x43, 0x73,
|
||||
0x2E, 0x6E, 0xFE, 0x26, 0xFE, 0x23, 0xFE, 0x26, 0x23, 0xAD, 0xA9, 0xA1, 0xE5, 0xEC, 0xF6, 0x45, 0x6D, 0x2E, 0x73,
|
||||
0x69, 0x6E, 0xFF, 0xC6, 0xFE, 0x12, 0xFE, 0x15, 0xFF, 0x64, 0xFE, 0x15, 0xA0, 0x03, 0x22, 0x21, 0x2E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x41, 0x65, 0xFF, 0x32, 0x42, 0x2E, 0x73, 0xFF, 0x2B, 0xFF, 0x2E, 0x23, 0x65, 0x61, 0x6F, 0xF9, 0xF9,
|
||||
0xF9, 0x41, 0x73, 0xFF, 0x20, 0x21, 0x6F, 0xFC, 0x41, 0x68, 0xD7, 0xC2, 0xC2, 0x00, 0xD1, 0x2E, 0x73, 0xFD, 0xDC,
|
||||
0xFD, 0xDF, 0x22, 0x6F, 0x61, 0xF7, 0xF7, 0x4C, 0x6F, 0xC3, 0x65, 0x61, 0x2E, 0x6D, 0x74, 0x6C, 0x73, 0x6E, 0x63,
|
||||
0x69, 0xFF, 0x7B, 0xFF, 0xB5, 0xFF, 0xBC, 0xFF, 0x24, 0xD3, 0xAA, 0xFF, 0xD2, 0xFF, 0xD5, 0xFF, 0xE0, 0xFF, 0xD5,
|
||||
0xFF, 0xEB, 0xFF, 0xEE, 0xFF, 0xFB, 0x41, 0x61, 0xFE, 0xFF, 0x43, 0x65, 0x6F, 0x61, 0xF0, 0x49, 0xF0, 0x49, 0xFB,
|
||||
0xBA, 0x43, 0x2E, 0x61, 0x65, 0xFD, 0x9B, 0xFD, 0xA1, 0xFE, 0xED, 0x43, 0x2E, 0x73, 0x72, 0xFD, 0x91, 0xFD, 0x94,
|
||||
0xFF, 0xF6, 0x48, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x6F, 0x61, 0x65, 0xD3, 0x63, 0xFC, 0xFE, 0xFC, 0xFE, 0xFF, 0xE2,
|
||||
0xFC, 0xE6, 0xFF, 0xF6, 0xFD, 0x8D, 0xE3, 0xDD, 0xA0, 0x05, 0x41, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
||||
0xFD, 0x41, 0x6E, 0xFD, 0x65, 0x21, 0xB3, 0xFC, 0x41, 0x65, 0xFE, 0xAD, 0x21, 0x6E, 0xFC, 0x22, 0xC3, 0x6F, 0xF6,
|
||||
0xFD, 0x43, 0x74, 0x61, 0x69, 0xE4, 0xD8, 0xFF, 0xEA, 0xFF, 0xFB, 0x41, 0x72, 0xE4, 0xCE, 0x41, 0x64, 0xFE, 0xBA,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x72, 0x69, 0xE4,
|
||||
0xB7, 0xFF, 0xFD, 0x41, 0x69, 0xE2, 0xB7, 0x41, 0x74, 0xED, 0x7B, 0x43, 0x64, 0x73, 0x74, 0xEA, 0xF2, 0xFF, 0xFC,
|
||||
0xE4, 0xA8, 0x41, 0x73, 0xEC, 0xE8, 0x42, 0x2E, 0x65, 0xD2, 0xF0, 0xFF, 0xFC, 0xA0, 0x08, 0xF1, 0x21, 0x2E, 0xFD,
|
||||
0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x73, 0xFD, 0x21, 0xAD, 0xFD, 0x53, 0x61, 0x69, 0x73, 0x2E,
|
||||
0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0x6F, 0x63, 0x65, 0x66, 0x67, 0x70, 0x75, 0x6C, 0xC3, 0xFE, 0x25, 0xFE, 0x38,
|
||||
0xFE, 0x59, 0xD2, 0xD2, 0xFE, 0x81, 0xFE, 0x8F, 0xFE, 0x9F, 0xFF, 0x28, 0xFF, 0x4D, 0xFF, 0x6F, 0xFB, 0x0B, 0xFF,
|
||||
0xA7, 0xFF, 0xB1, 0xFF, 0xC8, 0xFF, 0xB1, 0xFF, 0xCF, 0xFF, 0xD7, 0xFF, 0xE5, 0xFF, 0xFD, 0xA0, 0x01, 0xB2, 0x21,
|
||||
0xA1, 0xFD, 0x43, 0xC3, 0x65, 0x73, 0xFF, 0xFD, 0xD2, 0xF0, 0xE3, 0x8C, 0x41, 0x65, 0xEB, 0xDA, 0x21, 0x6C, 0xFC,
|
||||
0x41, 0x65, 0xE4, 0xF6, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x43, 0x2E, 0x63, 0x74, 0xD2, 0x77, 0xFF, 0xF3, 0xFF,
|
||||
0xFD, 0x41, 0x61, 0xD2, 0x6D, 0x21, 0x63, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x6F, 0xD5, 0x4C, 0x43, 0x6F, 0x62, 0x69,
|
||||
0xFD, 0xD5, 0xFF, 0xF9, 0xFF, 0xFC, 0xA0, 0x01, 0xB1, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x65, 0xFD, 0x43,
|
||||
0x65, 0x6F, 0x72, 0xEC, 0xC5, 0xD6, 0x64, 0xDE, 0x0C, 0x45, 0x2E, 0x68, 0x64, 0x65, 0x74, 0xD2, 0x3F, 0xFF, 0xF3,
|
||||
0xE3, 0xEC, 0xEB, 0xCF, 0xFF, 0xF6, 0xA0, 0x04, 0x13, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x42, 0x2E, 0x65, 0xFC, 0x44, 0xFF, 0xFD, 0xA0, 0x03, 0xC2, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x61, 0xED, 0x22, 0x61, 0x65, 0xEA, 0xEA, 0x46, 0x73, 0x2E, 0x6E, 0x69, 0x62, 0x72, 0xFF, 0xE8, 0xFC, 0x2C, 0xFC,
|
||||
0x2F, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFB, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFC, 0x19, 0xFC, 0x1C, 0xFD, 0xCD,
|
||||
0xFD, 0x6B, 0xFC, 0x1C, 0x42, 0x73, 0x61, 0xFC, 0x0C, 0xFF, 0xF0, 0x43, 0xA9, 0xA1, 0xAD, 0xFD, 0xD5, 0xFF, 0xD6,
|
||||
0xFF, 0xF9, 0xA0, 0x02, 0xF3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6D, 0xFD, 0x43, 0x65,
|
||||
0x61, 0x6F, 0xEE, 0x8D, 0xEE, 0x8D, 0xEE, 0x8D, 0x43, 0x2E, 0x73, 0x69, 0xFF, 0xA2, 0xFF, 0xA5, 0xFF, 0xA8, 0x21,
|
||||
0x65, 0xF6, 0xA0, 0x03, 0xE3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x24, 0x2E, 0x73, 0x69, 0x6E, 0xF7, 0xFA, 0xFD,
|
||||
0xFA, 0x43, 0x2E, 0x74, 0x65, 0xFF, 0x83, 0xFF, 0xEB, 0xFF, 0xF7, 0x21, 0x6F, 0xEA, 0x41, 0x65, 0xFF, 0x7C, 0x21,
|
||||
0x6E, 0xE0, 0x21, 0x73, 0xDA, 0x25, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0xDA, 0xF3, 0xFD, 0xDA, 0x22, 0x61, 0x73,
|
||||
0xF5, 0xCF, 0x23, 0x2E, 0x73, 0x69, 0xC7, 0xCA, 0xCD, 0x23, 0x73, 0x2E, 0x6E, 0xC3, 0xC0, 0xC3, 0x23, 0xAD, 0xA9,
|
||||
0xA1, 0xED, 0xF2, 0xF9, 0x45, 0x6D, 0x2E, 0x73, 0x69, 0x6E, 0xFF, 0xCE, 0xFF, 0xB2, 0xFF, 0xB5, 0xFF, 0xB8, 0xFF,
|
||||
0xB5, 0x44, 0x6F, 0xC3, 0x65, 0x61, 0xFF, 0xC5, 0xFF, 0xE9, 0xFF, 0xF0, 0xFF, 0xAB, 0xA0, 0x10, 0x54, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xEE, 0xF1,
|
||||
0xFD, 0x21, 0x65, 0xF9, 0x42, 0x61, 0x6C, 0xFF, 0x82, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFF, 0x72, 0xFF, 0x75, 0x43,
|
||||
0x2E, 0x61, 0x65, 0xFF, 0x6B, 0xFF, 0xF9, 0xFF, 0x71, 0x43, 0x2E, 0x73, 0x72, 0xFF, 0x61, 0xFF, 0x64, 0xFF, 0xF6,
|
||||
0x43, 0x2E, 0x6F, 0x61, 0xFE, 0xEC, 0xFF, 0xF6, 0xFF, 0xE5, 0x47, 0x73, 0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0xFF,
|
||||
0x5F, 0xFF, 0x69, 0xFE, 0xE5, 0xFF, 0x6C, 0xFF, 0xAB, 0xFF, 0xD4, 0xFF, 0xF6, 0x42, 0x2E, 0x65, 0xFB, 0x09, 0xFC,
|
||||
0x5B, 0x21, 0x64, 0xF9, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x45, 0x2E, 0x65, 0x61, 0x6D, 0x69, 0xFA, 0xF9, 0xFC,
|
||||
0x4B, 0xFA, 0xFF, 0xFB, 0x10, 0xFF, 0xFD, 0x21, 0x72, 0xF0, 0x21, 0x6F, 0xFD, 0x4B, 0xC3, 0x65, 0x2E, 0x6D, 0x74,
|
||||
0x6C, 0x73, 0x6E, 0x6F, 0x61, 0x69, 0xFE, 0xE1, 0xFE, 0xF7, 0xD0, 0xBF, 0xFA, 0x5A, 0xFA, 0x5A, 0xFE, 0xFA, 0xFA,
|
||||
0x5A, 0xFA, 0x42, 0xFC, 0x35, 0xFF, 0xC4, 0xFF, 0xFD, 0x46, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x72, 0xD0, 0x9D, 0xFA,
|
||||
0x38, 0xFA, 0x38, 0xFD, 0x1C, 0xFA, 0x20, 0xFA, 0xCC, 0x41, 0x61, 0xE1, 0x84, 0x21, 0x6C, 0xFC, 0x21, 0x64, 0xFD,
|
||||
0x41, 0x6F, 0xFB, 0x7E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x2E, 0xE3,
|
||||
0x46, 0x42, 0x2E, 0x73, 0xE3, 0x42, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x69, 0xFB, 0x23, 0x64, 0x6D,
|
||||
0x63, 0xD7, 0xEA, 0xFD, 0xA0, 0x00, 0x81, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA1, 0xFD,
|
||||
0x42, 0x6F, 0x72, 0xD4, 0x62, 0xDC, 0x11, 0xA0, 0x11, 0x92, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x44, 0x61, 0x6F, 0x74, 0x75, 0xE0, 0xA2,
|
||||
0xE9, 0x8F, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x6E, 0xE1, 0xC8, 0x42, 0x63, 0x72, 0xE1, 0xC4, 0xE1, 0xC4, 0x42, 0x6D,
|
||||
0x72, 0xD9, 0x69, 0xD4, 0xC3, 0x41, 0x67, 0xD9, 0xBC, 0x42, 0x61, 0x6F, 0xD0, 0x9B, 0xD0, 0x9B, 0x43, 0x61, 0x65,
|
||||
0x6F, 0xD0, 0x94, 0xD0, 0x94, 0xD0, 0x94, 0x21, 0x69, 0xF6, 0x44, 0x61, 0x69, 0x65, 0x6F, 0xD0, 0x87, 0xD0, 0x87,
|
||||
0xD0, 0x87, 0xD0, 0x87, 0x44, 0x6A, 0x67, 0x6C, 0x6D, 0xFF, 0xDF, 0xD9, 0x97, 0xFF, 0xF0, 0xFF, 0xF3, 0x44, 0xA1,
|
||||
0xA9, 0xB3, 0xAD, 0xFF, 0xC7, 0xFF, 0xCE, 0xD8, 0x5C, 0xFF, 0xF3, 0x41, 0x72, 0xDC, 0x2A, 0x21, 0x65, 0xFC, 0x41,
|
||||
0x6D, 0xE2, 0x99, 0x21, 0x75, 0xFC, 0x41, 0x69, 0xD1, 0xE0, 0x44, 0x63, 0x67, 0x6C, 0x6D, 0xFF, 0xF2, 0xD9, 0x83,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xD2, 0x2C, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x41, 0x69, 0xD7, 0xE1, 0x21,
|
||||
0x75, 0xFC, 0x43, 0x63, 0x67, 0x71, 0xD1, 0xB0, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x61, 0xC3, 0x6F, 0xD1, 0xA3, 0xD9,
|
||||
0x2F, 0xD1, 0xA3, 0x41, 0xAD, 0xD1, 0x99, 0x43, 0x65, 0x69, 0xC3, 0xD1, 0x95, 0xD1, 0x95, 0xFF, 0xFC, 0x42, 0x69,
|
||||
0x61, 0xD7, 0x0B, 0xD1, 0x8E, 0x44, 0xA1, 0xAD, 0xA9, 0xB3, 0xD1, 0x84, 0xD1, 0x84, 0xD1, 0x84, 0xD1, 0x84, 0x45,
|
||||
0x61, 0xC3, 0x69, 0x65, 0x6F, 0xD1, 0x77, 0xFF, 0xF3, 0xD1, 0x77, 0xD1, 0x77, 0xD1, 0x77, 0x41, 0x6F, 0xD1, 0xE6,
|
||||
0x25, 0x6A, 0x67, 0x6C, 0x6D, 0x74, 0xC0, 0xCE, 0xD8, 0xEC, 0xFC, 0x41, 0xB3, 0xD1, 0x58, 0x21, 0xC3, 0xFC, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x72, 0xFF, 0x7F, 0x41, 0xA9, 0xD1, 0x4D, 0x43, 0x63, 0x71, 0x73, 0xD1, 0x46, 0xD1, 0x46, 0xD6,
|
||||
0x27, 0x22, 0xC3, 0x69, 0xF2, 0xF6, 0x41, 0x6D, 0xD1, 0xA5, 0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF9, 0xFD, 0x41,
|
||||
0x71, 0xD1, 0x2B, 0x21, 0x73, 0xFC, 0x41, 0x61, 0xD1, 0x35, 0x21, 0x6C, 0xFC, 0x47, 0x62, 0x6E, 0x63, 0x74, 0x67,
|
||||
0x65, 0x70, 0xFF, 0xCC, 0xD8, 0xD5, 0xFF, 0xCF, 0xFF, 0xE1, 0xFF, 0xED, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x72, 0x74,
|
||||
0x63, 0xD1, 0x07, 0xD1, 0x07, 0xD1, 0x07, 0x21, 0x61, 0xF6, 0x42, 0x62, 0x64, 0xD8, 0xB2, 0xFF, 0xFD, 0x41, 0x72,
|
||||
0xD0, 0x12, 0xA0, 0x0D, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x48, 0xC3, 0x61, 0x65, 0x69,
|
||||
0x6F, 0x75, 0x74, 0x70, 0xFE, 0xF9, 0xFF, 0x18, 0xFF, 0x36, 0xFF, 0x80, 0xFF, 0xC6, 0xFF, 0xE9, 0xFF, 0xF0, 0xFF,
|
||||
0xFD, 0x41, 0x72, 0xFA, 0x54, 0x21, 0x74, 0xFC, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x65, 0xC3, 0xFA, 0xFD,
|
||||
0x4F, 0x6F, 0x73, 0x2E, 0x6D, 0x6E, 0x72, 0x64, 0x65, 0x61, 0xC3, 0x63, 0x74, 0x75, 0x78, 0x6C, 0xFC, 0x13, 0xFC,
|
||||
0x2E, 0xCE, 0xA5, 0xFC, 0x46, 0xFC, 0x66, 0xFD, 0xE6, 0xFE, 0x08, 0xFE, 0x22, 0xFE, 0x48, 0xFE, 0x5B, 0xFE, 0x7D,
|
||||
0xFE, 0x8A, 0xFE, 0x8E, 0xFF, 0xD5, 0xFF, 0xFB, 0x43, 0x2E, 0x73, 0x6D, 0xF8, 0x9B, 0xF8, 0x9E, 0xFA, 0x4F, 0xA0,
|
||||
0x03, 0x53, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x84, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xF0, 0xF0, 0xFB, 0x22, 0x2E, 0x6C, 0xE3, 0xF9, 0x21, 0x65, 0xFB, 0x23, 0x65,
|
||||
0x61, 0x6F, 0xE1, 0xE1, 0xE1, 0x23, 0x65, 0x6F, 0x61, 0xDA, 0xDA, 0xDA, 0x21, 0x6C, 0xF9, 0x24, 0x6D, 0x74, 0x6C,
|
||||
0x65, 0xEC, 0xEC, 0xEF, 0xFD, 0x22, 0x2E, 0x6C, 0xC1, 0xED, 0x21, 0x73, 0xFB, 0x21, 0x6F, 0xFD, 0x23, 0x73, 0x6E,
|
||||
0x6F, 0xEC, 0xFD, 0xFA, 0x21, 0x6F, 0xF9, 0x43, 0x73, 0x69, 0x6D, 0xF8, 0x40, 0xF9, 0x8F, 0xFF, 0xFD, 0x21, 0xA1,
|
||||
0xF6, 0x43, 0x6F, 0x61, 0xC3, 0xF8, 0x33, 0xFF, 0x95, 0xFF, 0xFD, 0x41, 0x69, 0xF9, 0x78, 0x21, 0x74, 0xFC, 0x41,
|
||||
0x73, 0xF8, 0xEC, 0x42, 0x2E, 0x65, 0xF8, 0xE5, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x43, 0x69, 0x61, 0x65, 0xFF, 0xEF,
|
||||
0xFF, 0xFD, 0xF8, 0x1C, 0x41, 0x61, 0xEA, 0xAB, 0x42, 0x6F, 0x69, 0xCE, 0x5D, 0xCE, 0xBE, 0x21, 0x6D, 0xF9, 0x41,
|
||||
0x69, 0xCE, 0xB4, 0x21, 0x6D, 0xFC, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x44, 0x6D, 0x74, 0x6C, 0x6F,
|
||||
0xF6, 0xB8, 0xFF, 0xE3, 0xFF, 0xFB, 0xE7, 0x2D, 0xC3, 0x02, 0x91, 0x61, 0xC3, 0x65, 0xCF, 0xCC, 0xD7, 0x58, 0xCF,
|
||||
0xCC, 0x21, 0x69, 0xF4, 0x21, 0x63, 0xFD, 0xC1, 0x05, 0x81, 0x61, 0xCE, 0x3D, 0x21, 0x69, 0xFA, 0x21, 0x63, 0xFD,
|
||||
0x21, 0xAD, 0xFD, 0xA0, 0x02, 0xB1, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x23, 0x62, 0x65, 0xC3,
|
||||
0xF4, 0xFA, 0xFD, 0x21, 0x62, 0xED, 0x21, 0x6C, 0xEA, 0x21, 0x6D, 0xE7, 0x21, 0x69, 0xE7, 0x21, 0x70, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0x25, 0xAD, 0xA1, 0xA9, 0xBA, 0xB3, 0xEE, 0xF1, 0xE1, 0xF4, 0xFD, 0x22, 0x74, 0x69, 0xD0, 0xD0, 0x21,
|
||||
0x6E, 0xCE, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x72, 0xF5, 0xFD, 0x25, 0x69, 0xC3, 0x61, 0x65, 0x75, 0xCC, 0xE5, 0xD6,
|
||||
0xFB, 0xD9, 0x41, 0x75, 0xEA, 0x4B, 0xA0, 0x0B, 0xE3, 0x22, 0x75, 0x74, 0xFD, 0xFD, 0x22, 0x73, 0x64, 0xFB, 0xF8,
|
||||
0xA0, 0x0C, 0x63, 0x21, 0x72, 0xFD, 0xA0, 0x0C, 0x93, 0x21, 0x2E, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xEF, 0xF7, 0xFD,
|
||||
0x41, 0x73, 0xEA, 0x44, 0x21, 0xA9, 0xFC, 0xA0, 0x0A, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0xA0, 0x0C, 0x42, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x65, 0xFD, 0x24, 0x69, 0xC3, 0x65,
|
||||
0x72, 0xD7, 0xE2, 0xEE, 0xFD, 0x21, 0x72, 0xF7, 0x42, 0x65, 0x72, 0xFF, 0xFD, 0xCE, 0x2D, 0x41, 0x72, 0xFC, 0xB4,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x72, 0xCD, 0xC6, 0x21, 0x65, 0xFC, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0xC3, 0x68, 0x66, 0x6D, 0x74, 0x6F, 0x61, 0x64, 0x67, 0xFF, 0x2D,
|
||||
0xFF, 0x3C, 0xFF, 0x7F, 0xFD, 0xF7, 0xFF, 0x8A, 0xFF, 0xDC, 0xE9, 0x9F, 0xE9, 0x9F, 0xFF, 0xED, 0xFF, 0xFD, 0x41,
|
||||
0x65, 0xD8, 0x86, 0x43, 0x6E, 0x2E, 0x73, 0xD8, 0x7E, 0xF7, 0x21, 0xF7, 0x24, 0x42, 0x6F, 0x61, 0xFF, 0xF6, 0xF7,
|
||||
0x1D, 0x42, 0x2E, 0x73, 0xF8, 0xC5, 0xF8, 0xC8, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x41, 0x2E, 0xEE, 0x81, 0x21, 0x73,
|
||||
0xFC, 0x21, 0x65, 0xFD, 0x44, 0x6E, 0x72, 0x2E, 0x73, 0xFF, 0xF1, 0xFF, 0xFD, 0xF7, 0x72, 0xF7, 0x75, 0x41, 0x61,
|
||||
0xDE, 0x29, 0x41, 0x72, 0xF8, 0xA1, 0x42, 0x2E, 0x73, 0xF7, 0x5D, 0xF7, 0x60, 0x4A, 0x67, 0x64, 0x73, 0x6E, 0x62,
|
||||
0x63, 0x61, 0x74, 0x65, 0x6F, 0xFE, 0x65, 0xFE, 0x84, 0xFE, 0xAB, 0xFF, 0x9A, 0xFF, 0xB9, 0xFF, 0xC7, 0xFF, 0xE4,
|
||||
0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xF9, 0x41, 0x69, 0xFB, 0xFC, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x74, 0xFD,
|
||||
0x68, 0xA0, 0x11, 0xB2, 0x42, 0x64, 0x74, 0xFF, 0xFD, 0xFC, 0x01, 0x21, 0x69, 0xF9, 0x21, 0x73, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x74, 0x6C, 0x6E, 0xDD, 0xE0, 0xFD, 0x5C, 0x62,
|
||||
0x2E, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0xC3, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xCD, 0x5C, 0xDB, 0x8C, 0xDD, 0xF1, 0xE3, 0xC6, 0xE4, 0x43, 0xE5,
|
||||
0xC4, 0xE7, 0x42, 0xE7, 0x94, 0xE7, 0xDD, 0xE8, 0x9B, 0xE9, 0xD6, 0xEA, 0x67, 0xED, 0x21, 0xED, 0x83, 0xEE, 0x2C,
|
||||
0xF0, 0x08, 0xF2, 0x7F, 0xF2, 0xDD, 0xF3, 0x29, 0xF3, 0x78, 0xF3, 0xC3, 0xF4, 0x0C, 0xF6, 0x24, 0xF7, 0x4C, 0xF9,
|
||||
0x4F, 0xFD, 0x7C, 0xFF, 0xB0, 0xFF, 0xF9,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns es_patterns = {
|
||||
es_trie_data,
|
||||
sizeof(es_trie_data),
|
||||
};
|
||||
@@ -10,8 +10,8 @@
|
||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
|
||||
// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it
|
||||
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
||||
constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB
|
||||
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
@@ -22,6 +22,9 @@ constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
||||
const char* ITALIC_TAGS[] = {"i", "em"};
|
||||
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"};
|
||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
|
||||
@@ -40,18 +43,74 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
||||
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
|
||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
// 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;
|
||||
if (isBold) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::BOLD);
|
||||
}
|
||||
if (isItalic) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
||||
}
|
||||
if (isUnderline) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
||||
}
|
||||
|
||||
// flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -63,42 +122,63 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
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
|
||||
if (strcmp(name, "table") == 0) {
|
||||
// Add placeholder text
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
if (self->currentTextBlock) {
|
||||
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
|
||||
}
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
|
||||
// Skip table contents
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for an element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
std::string alt;
|
||||
std::string alt = "[Image]";
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "alt") == 0) {
|
||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
||||
if (strlen(atts[i + 1]) > 0) {
|
||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
|
||||
} else {
|
||||
// Skip for now
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for an element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||
@@ -120,24 +200,142 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
|
||||
// Compute CSS style for this element
|
||||
CssStyle cssStyle;
|
||||
if (self->cssParser) {
|
||||
// Get combined tag + class styles
|
||||
cssStyle = self->cssParser->resolveStyle(name, classAttr);
|
||||
// Merge inline style (highest priority)
|
||||
if (!styleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||
cssStyle.applyOver(inlineStyle);
|
||||
}
|
||||
}
|
||||
|
||||
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->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
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) {
|
||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
|
||||
} else {
|
||||
self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment);
|
||||
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)) {
|
||||
// Flush buffer before style change so preceding text gets current style
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
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->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
// Push inline style entry for bold tag
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
entry.hasBold = true;
|
||||
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);
|
||||
// Push inline style entry for italic tag
|
||||
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
|
||||
self->depth += 1;
|
||||
}
|
||||
|
||||
@@ -149,23 +347,14 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
return;
|
||||
}
|
||||
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (self->boldUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (isWhitespace(s[i])) {
|
||||
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle);
|
||||
self->partWordBufferIndex = 0;
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
// Whitespace is a real word boundary — reset continuation state
|
||||
self->nextWordContinues = false;
|
||||
// Skip the whitespace char
|
||||
continue;
|
||||
}
|
||||
@@ -186,9 +375,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
|
||||
// If we're about to run out of space, then cut the word off and start a new one
|
||||
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle);
|
||||
self->partWordBufferIndex = 0;
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
@@ -209,28 +396,34 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
// Check if any style state will change after we decrement depth
|
||||
// If so, we MUST flush the partWordBuffer with the CURRENT style first
|
||||
// Note: depth hasn't been decremented yet, so we check against (depth - 1)
|
||||
const bool willPopStyleStack =
|
||||
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
||||
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
||||
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
||||
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
||||
|
||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||
|
||||
// Flush buffer with current style BEFORE any style changes
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
||||
// We don't want to flush out content when closing inline tags like <span>.
|
||||
// Currently this also flushes out on closing <b> and <i> tags, but they are line tags so that shouldn't happen,
|
||||
// text styling needs to be overhauled to fix it.
|
||||
const bool shouldBreakText =
|
||||
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
||||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
|
||||
// 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 (shouldBreakText) {
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (self->boldUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
if (shouldFlush) {
|
||||
self->flushPartWordBuffer();
|
||||
// If closing an inline element, the next word fragment continues the same visual word
|
||||
if (isInlineTag) {
|
||||
self->nextWordContinues = true;
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle);
|
||||
self->partWordBufferIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,19 +434,44 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
self->skipUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving bold
|
||||
// Leaving bold tag
|
||||
if (self->boldUntilDepth == self->depth) {
|
||||
self->boldUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving italic
|
||||
// Leaving italic tag
|
||||
if (self->italicUntilDepth == self->depth) {
|
||||
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() {
|
||||
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);
|
||||
int done;
|
||||
@@ -269,10 +487,10 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get file size for progress calculation
|
||||
const size_t totalSize = file.size();
|
||||
size_t bytesRead = 0;
|
||||
int lastProgress = -1;
|
||||
// Get file size to decide whether to show indexing popup.
|
||||
if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
|
||||
popupFn();
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
@@ -302,17 +520,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
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;
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
@@ -353,7 +560,9 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -369,10 +578,34 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
}
|
||||
|
||||
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(
|
||||
renderer, fontId, viewportWidth,
|
||||
renderer, fontId, effectiveWidth,
|
||||
[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) {
|
||||
currentPageNextY += lineHeight / 2;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "../ParsedText.h"
|
||||
#include "../blocks/TextBlock.h"
|
||||
#include "../css/CssParser.h"
|
||||
#include "../css/CssStyle.h"
|
||||
|
||||
class Page;
|
||||
class GfxRenderer;
|
||||
@@ -18,15 +20,17 @@ class ChapterHtmlSlimParser {
|
||||
const std::string& filepath;
|
||||
GfxRenderer& renderer;
|
||||
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 skipUntilDepth = INT_MAX;
|
||||
int boldUntilDepth = INT_MAX;
|
||||
int italicUntilDepth = INT_MAX;
|
||||
int underlineUntilDepth = INT_MAX;
|
||||
// buffer for building up words from characters, will auto break if longer than this
|
||||
// leave one char at end for null pointer
|
||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||
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<Page> currentPage = nullptr;
|
||||
int16_t currentPageNextY = 0;
|
||||
@@ -37,8 +41,25 @@ class ChapterHtmlSlimParser {
|
||||
uint16_t viewportWidth;
|
||||
uint16_t viewportHeight;
|
||||
bool hyphenationEnabled;
|
||||
const CssParser* cssParser;
|
||||
bool embeddedStyle;
|
||||
|
||||
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 makePages();
|
||||
// XML callbacks
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
@@ -51,7 +72,9 @@ class ChapterHtmlSlimParser {
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||
const std::function<void(int)>& progressFn = nullptr)
|
||||
const bool embeddedStyle, const std::function<void()>& popupFn = nullptr,
|
||||
const CssParser* cssParser = nullptr)
|
||||
|
||||
: filepath(filepath),
|
||||
renderer(renderer),
|
||||
fontId(fontId),
|
||||
@@ -62,7 +85,10 @@ class ChapterHtmlSlimParser {
|
||||
viewportHeight(viewportHeight),
|
||||
hyphenationEnabled(hyphenationEnabled),
|
||||
completePageFn(completePageFn),
|
||||
progressFn(progressFn) {}
|
||||
popupFn(popupFn),
|
||||
cssParser(cssParser),
|
||||
embeddedStyle(embeddedStyle) {}
|
||||
|
||||
~ChapterHtmlSlimParser() = default;
|
||||
bool parseAndBuildPages();
|
||||
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
namespace {
|
||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||
constexpr char itemCacheFile[] = "/.items.bin";
|
||||
} // namespace
|
||||
|
||||
@@ -38,6 +39,9 @@ ContentOpfParser::~ContentOpfParser() {
|
||||
if (SdMan.exists((cachePath + itemCacheFile).c_str())) {
|
||||
SdMan.remove((cachePath + itemCacheFile).c_str());
|
||||
}
|
||||
itemIndex.clear();
|
||||
itemIndex.shrink_to_fit();
|
||||
useItemIndex = false;
|
||||
}
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
@@ -129,6 +133,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
"[%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
|
||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
self->useItemIndex = true;
|
||||
Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,6 +193,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
}
|
||||
}
|
||||
|
||||
// Record index entry for fast lookup later
|
||||
if (self->tempItemStore) {
|
||||
ItemIndexEntry entry;
|
||||
entry.idHash = fnvHash(itemId);
|
||||
entry.idLen = static_cast<uint16_t>(itemId.size());
|
||||
entry.fileOffset = static_cast<uint32_t>(self->tempItemStore.position());
|
||||
self->itemIndex.push_back(entry);
|
||||
}
|
||||
|
||||
// Write items down to SD card
|
||||
serialization::writeString(self->tempItemStore, itemId);
|
||||
serialization::writeString(self->tempItemStore, href);
|
||||
@@ -197,6 +219,11 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
}
|
||||
}
|
||||
|
||||
// Collect CSS files
|
||||
if (mediaType == MEDIA_TYPE_CSS) {
|
||||
self->cssFiles.push_back(href);
|
||||
}
|
||||
|
||||
// EPUB 3: Check for nav document (properties contains "nav")
|
||||
if (!properties.empty() && self->tocNavPath.empty()) {
|
||||
// Properties is space-separated, check if "nav" is present as a word
|
||||
@@ -215,19 +242,50 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "idref") == 0) {
|
||||
const std::string idref = atts[i + 1];
|
||||
// Resolve the idref to href using items map
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
std::string href;
|
||||
while (self->tempItemStore.available()) {
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
if (itemId == idref) {
|
||||
self->cache->createSpineEntry(href);
|
||||
break;
|
||||
bool found = false;
|
||||
|
||||
if (self->useItemIndex) {
|
||||
// Fast path: binary search
|
||||
uint32_t targetHash = fnvHash(idref);
|
||||
uint16_t targetLen = static_cast<uint16_t>(idref.size());
|
||||
|
||||
auto it = std::lower_bound(self->itemIndex.begin(), self->itemIndex.end(),
|
||||
ItemIndexEntry{targetHash, targetLen, 0},
|
||||
[](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
|
||||
// Check for match (may need to check a few due to hash collisions)
|
||||
while (it != self->itemIndex.end() && it->idHash == targetHash) {
|
||||
self->tempItemStore.seek(it->fileOffset);
|
||||
std::string itemId;
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
if (itemId == idref) {
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
while (self->tempItemStore.available()) {
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
if (itemId == idref) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found && self->cache) {
|
||||
self->cache->createSpineEntry(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub.h"
|
||||
#include "expat.h"
|
||||
|
||||
@@ -28,6 +31,27 @@ class ContentOpfParser final : public Print {
|
||||
FsFile tempItemStore;
|
||||
std::string coverItemId;
|
||||
|
||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
||||
struct ItemIndexEntry {
|
||||
uint32_t idHash; // FNV-1a hash of itemId
|
||||
uint16_t idLen; // length for collision reduction
|
||||
uint32_t fileOffset; // offset in .items.bin
|
||||
};
|
||||
std::vector<ItemIndexEntry> itemIndex;
|
||||
bool useItemIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a hash function
|
||||
static uint32_t fnvHash(const std::string& s) {
|
||||
uint32_t hash = 2166136261u;
|
||||
for (char c : s) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
@@ -40,6 +64,7 @@ class ContentOpfParser final : public Print {
|
||||
std::string tocNavPath; // EPUB 3 nav document path
|
||||
std::string coverItemHref;
|
||||
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,
|
||||
BookMetadataCache* cache)
|
||||
|
||||
+341
-153
@@ -2,62 +2,68 @@
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
void GfxRenderer::begin() {
|
||||
frameBuffer = display.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
case Portrait: {
|
||||
case GfxRenderer::Portrait: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
*rotatedX = y;
|
||||
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
*phyX = y;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
break;
|
||||
}
|
||||
case LandscapeClockwise: {
|
||||
case GfxRenderer::LandscapeClockwise: {
|
||||
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
||||
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x;
|
||||
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||
break;
|
||||
}
|
||||
case PortraitInverted: {
|
||||
case GfxRenderer::PortraitInverted: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees counter-clockwise
|
||||
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y;
|
||||
*rotatedY = x;
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
||||
*phyY = x;
|
||||
break;
|
||||
}
|
||||
case LandscapeCounterClockwise: {
|
||||
case GfxRenderer::LandscapeCounterClockwise: {
|
||||
// Logical landscape (800x480) aligned with panel orientation
|
||||
*rotatedX = x;
|
||||
*rotatedY = y;
|
||||
*phyX = x;
|
||||
*phyY = y;
|
||||
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 {
|
||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||
int phyX = 0;
|
||||
int phyY = 0;
|
||||
|
||||
// Early return if no framebuffer is set
|
||||
if (!frameBuffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
int rotatedX = 0;
|
||||
int rotatedY = 0;
|
||||
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
||||
// Note: this call should be inlined for better performance
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY);
|
||||
|
||||
// Bounds checking against physical panel dimensions
|
||||
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
||||
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
|
||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
|
||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, phyX, phyY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate byte position and bit position
|
||||
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
||||
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
||||
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX / 8);
|
||||
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||
|
||||
if (state) {
|
||||
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||
@@ -131,6 +137,12 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||
drawLine(x, y, x + width - 1, y, state);
|
||||
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
|
||||
@@ -138,18 +150,261 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
|
||||
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 {
|
||||
for (int fillY = y; fillY < y + height; fillY++) {
|
||||
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 {
|
||||
// TODO: Rotate bits
|
||||
int rotatedX = 0;
|
||||
int rotatedY = 0;
|
||||
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
||||
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY);
|
||||
// Rotate origin corner
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
rotatedY = rotatedY - height;
|
||||
break;
|
||||
case PortraitInverted:
|
||||
rotatedX = rotatedX - width;
|
||||
break;
|
||||
case LandscapeClockwise:
|
||||
rotatedY = rotatedY - height;
|
||||
rotatedX = rotatedX - width;
|
||||
break;
|
||||
case LandscapeCounterClockwise:
|
||||
break;
|
||||
}
|
||||
// TODO: Rotate bits
|
||||
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,
|
||||
@@ -384,32 +639,43 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
|
||||
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 {
|
||||
uint8_t* buffer = einkDisplay.getFrameBuffer();
|
||||
if (!buffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
||||
buffer[i] = ~buffer[i];
|
||||
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
||||
frameBuffer[i] = ~frameBuffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
|
||||
einkDisplay.displayBuffer(refreshMode);
|
||||
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
||||
auto elapsed = millis() - start_ms;
|
||||
Serial.printf("[%lu] [GFX] Time = %lu ms from clearScreen to displayBuffer\n", millis(), elapsed);
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
}
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
if (!text || maxWidth <= 0) return "";
|
||||
|
||||
std::string item = text;
|
||||
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
while (itemWidth > maxWidth && item.length() > 8) {
|
||||
item.replace(item.length() - 5, 5, "...");
|
||||
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
const char* ellipsis = "...";
|
||||
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
if (textWidth <= maxWidth) {
|
||||
// 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
|
||||
@@ -418,13 +684,13 @@ int GfxRenderer::getScreenWidth() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 480px wide in portrait logical coordinates
|
||||
return EInkDisplay::DISPLAY_HEIGHT;
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 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 {
|
||||
@@ -432,13 +698,13 @@ int GfxRenderer::getScreenHeight() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 800px tall in portrait logical coordinates
|
||||
return EInkDisplay::DISPLAY_WIDTH;
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 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 {
|
||||
@@ -450,6 +716,20 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
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 {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
@@ -468,85 +748,6 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
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 {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
@@ -638,17 +839,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() {
|
||||
for (auto& bwBufferChunk : bwBufferChunks) {
|
||||
@@ -666,12 +868,6 @@ void GfxRenderer::freeBwBufferChunks() {
|
||||
* Returns true if buffer was stored successfully, false if allocation failed.
|
||||
*/
|
||||
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
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
// Check if any chunks are already allocated
|
||||
@@ -721,13 +917,6 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
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++) {
|
||||
// Check if chunk is missing
|
||||
if (!bwBufferChunks[i]) {
|
||||
@@ -740,7 +929,7 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||
|
||||
freeBwBufferChunks();
|
||||
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
|
||||
@@ -751,9 +940,8 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
* Use this when BW buffer was re-rendered instead of stored/restored.
|
||||
*/
|
||||
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||
if (frameBuffer) {
|
||||
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
#include <HalDisplay.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#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 {
|
||||
public:
|
||||
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||
@@ -21,22 +25,28 @@ class GfxRenderer {
|
||||
|
||||
private:
|
||||
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_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_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 == HalDisplay::BUFFER_SIZE,
|
||||
"BW buffer chunking does not line up with display buffer size");
|
||||
|
||||
EInkDisplay& einkDisplay;
|
||||
HalDisplay& display;
|
||||
RenderMode renderMode;
|
||||
Orientation orientation;
|
||||
bool fadingFix;
|
||||
uint8_t* frameBuffer = nullptr;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
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:
|
||||
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
||||
explicit GfxRenderer(HalDisplay& halDisplay)
|
||||
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
|
||||
~GfxRenderer() { freeBwBufferChunks(); }
|
||||
|
||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
||||
@@ -45,27 +55,43 @@ class GfxRenderer {
|
||||
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
||||
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
void setOrientation(const Orientation o) { orientation = o; }
|
||||
Orientation getOrientation() const { return orientation; }
|
||||
|
||||
// Fading fix control
|
||||
void setFadingFix(const bool enabled) { fadingFix = enabled; }
|
||||
|
||||
// Screen ops
|
||||
int getScreenWidth() 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
|
||||
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 clearScreen(uint8_t color = 0xFF) const;
|
||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||
|
||||
// Drawing
|
||||
void drawPixel(int x, int y, bool state = true) const;
|
||||
void 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, 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 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 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,
|
||||
float cropY = 0) const;
|
||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||
@@ -78,22 +104,17 @@ class GfxRenderer {
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getSpaceWidth(int fontId) const;
|
||||
int getTextAdvanceX(int fontId, const char* text) const;
|
||||
int getFontAscenderSize(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
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)
|
||||
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getTextHeight(int fontId) const;
|
||||
|
||||
public:
|
||||
// Grayscale functions
|
||||
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
||||
void copyGrayscaleLsbBuffers() const;
|
||||
@@ -106,6 +127,4 @@ class GfxRenderer {
|
||||
// Low level functions
|
||||
uint8_t* getFrameBuffer() const;
|
||||
static size_t getBufferSize();
|
||||
void grayscaleRevert() const;
|
||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||
};
|
||||
|
||||
@@ -200,7 +200,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
|
||||
|
||||
// Internal implementation with configurable target size and bit depth
|
||||
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||
bool oneBit) {
|
||||
bool oneBit, bool crop) {
|
||||
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit",
|
||||
targetWidth, targetHeight);
|
||||
|
||||
@@ -242,8 +242,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
|
||||
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
|
||||
// We scale to the smaller dimension, so we can potentially crop later.
|
||||
// TODO: ideally, we already crop here.
|
||||
const float scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
float scale = 1.0;
|
||||
if (crop) { // if we will crop, scale to the smaller dimension
|
||||
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
} else { // else, scale to the larger dimension to fit
|
||||
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
}
|
||||
|
||||
outWidth = static_cast<int>(imageInfo.m_width * scale);
|
||||
outHeight = static_cast<int>(imageInfo.m_height * scale);
|
||||
@@ -550,8 +554,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
}
|
||||
|
||||
// Core function: Convert JPEG file to 2-bit BMP (uses default target size)
|
||||
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
|
||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false);
|
||||
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop) {
|
||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop);
|
||||
}
|
||||
|
||||
// Convert with custom target size (for thumbnails, 2-bit)
|
||||
@@ -563,5 +567,5 @@ bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bm
|
||||
// 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,
|
||||
int targetMaxHeight) {
|
||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
|
||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ class JpegToBmpConverter {
|
||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||
static bool jpegFileToBmpStreamInternal(class FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||
bool oneBit);
|
||||
bool oneBit, bool crop = true);
|
||||
|
||||
public:
|
||||
static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut);
|
||||
static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop = true);
|
||||
// Convert with custom target size (for thumbnails)
|
||||
static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
|
||||
|
||||
@@ -33,10 +33,10 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat
|
||||
|
||||
size_t KOReaderDocumentId::getOffset(int i) {
|
||||
// Offset = 1024 << (2*i)
|
||||
// For i = -1: 1024 >> 2 = 256
|
||||
// For i = -1: KOReader uses a value of 0
|
||||
// For i >= 0: 1024 << (2*i)
|
||||
if (i < 0) {
|
||||
return CHUNK_SIZE >> (-2 * i);
|
||||
return 0;
|
||||
}
|
||||
return CHUNK_SIZE << (2 * i);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ void addAuthHeaders(HTTPClient& http) {
|
||||
http.addHeader("Accept", "application/vnd.koreader.v1+json");
|
||||
http.addHeader("x-auth-user", KOREADER_STORE.getUsername().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; }
|
||||
|
||||
@@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <string>
|
||||
#define REPLACEMENT_GLYPH 0xFFFD
|
||||
|
||||
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);
|
||||
|
||||
+20
-11
@@ -7,7 +7,6 @@
|
||||
|
||||
#include "Xtc.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
@@ -87,6 +86,15 @@ std::string Xtc::getTitle() const {
|
||||
return filepath.substr(lastSlash, lastDot - lastSlash);
|
||||
}
|
||||
|
||||
std::string Xtc::getAuthor() const {
|
||||
if (!loaded || !parser) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try to get author from XTC metadata
|
||||
return parser->getAuthor();
|
||||
}
|
||||
|
||||
bool Xtc::hasChapters() const {
|
||||
if (!loaded || !parser) {
|
||||
return false;
|
||||
@@ -293,11 +301,12 @@ bool Xtc::generateCoverBmp() const {
|
||||
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
|
||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
||||
if (SdMan.exists(getThumbBmpPath(height).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -325,13 +334,13 @@ bool Xtc::generateThumbBmp() const {
|
||||
const uint8_t bitDepth = parser->getBitDepth();
|
||||
|
||||
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card)
|
||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
|
||||
// Calculate scale factor
|
||||
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
|
||||
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
|
||||
if (scale >= 1.0f) {
|
||||
@@ -340,7 +349,7 @@ bool Xtc::generateThumbBmp() const {
|
||||
if (generateCoverBmp()) {
|
||||
FsFile src, dst;
|
||||
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) {
|
||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(height), dst)) {
|
||||
uint8_t buffer[512];
|
||||
while (src.available()) {
|
||||
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
||||
@@ -351,7 +360,7 @@ bool Xtc::generateThumbBmp() const {
|
||||
src.close();
|
||||
}
|
||||
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
|
||||
return SdMan.exists(getThumbBmpPath().c_str());
|
||||
return SdMan.exists(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -385,7 +394,7 @@ bool Xtc::generateThumbBmp() const {
|
||||
|
||||
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
||||
FsFile thumbBmp;
|
||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) {
|
||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
@@ -550,7 +559,7 @@ bool Xtc::generateThumbBmp() const {
|
||||
free(pageBuffer);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
|
||||
getThumbBmpPath().c_str());
|
||||
getThumbBmpPath(height).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -56,6 +56,7 @@ class Xtc {
|
||||
|
||||
// Metadata
|
||||
std::string getTitle() const;
|
||||
std::string getAuthor() const;
|
||||
bool hasChapters() const;
|
||||
const std::vector<xtc::ChapterInfo>& getChapters() const;
|
||||
|
||||
@@ -64,7 +65,8 @@ class Xtc {
|
||||
bool generateCoverBmp() const;
|
||||
// Thumbnail support (for Continue Reading card)
|
||||
std::string getThumbBmpPath() const;
|
||||
bool generateThumbBmp() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
bool generateThumbBmp(int height) const;
|
||||
|
||||
// Page access
|
||||
uint32_t getPageCount() const;
|
||||
|
||||
+33
-10
@@ -47,8 +47,21 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
// Read title if available
|
||||
readTitle();
|
||||
// Read title & author if available
|
||||
if (m_header.hasMetadata) {
|
||||
m_lastError = readTitle();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
m_lastError = readAuthor();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
}
|
||||
|
||||
// Read page table
|
||||
m_lastError = readPageTable();
|
||||
@@ -124,24 +137,34 @@ XtcError XtcParser::readHeader() {
|
||||
}
|
||||
|
||||
XtcError XtcParser::readTitle() {
|
||||
// Title is usually at offset 0x38 (56) for 88-byte headers
|
||||
// Read title as null-terminated UTF-8 string
|
||||
if (m_header.titleOffset == 0) {
|
||||
m_header.titleOffset = 0x38; // Default offset
|
||||
}
|
||||
|
||||
if (!m_file.seek(m_header.titleOffset)) {
|
||||
constexpr auto titleOffset = 0x38;
|
||||
if (!m_file.seek(titleOffset)) {
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
char titleBuf[128] = {0};
|
||||
m_file.read(reinterpret_cast<uint8_t*>(titleBuf), sizeof(titleBuf) - 1);
|
||||
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
||||
m_title = titleBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
XtcError XtcParser::readAuthor() {
|
||||
// Read author as null-terminated UTF-8 string with max length 64, directly following title
|
||||
constexpr auto authorOffset = 0xB8;
|
||||
if (!m_file.seek(authorOffset)) {
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
char authorBuf[64] = {0};
|
||||
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
||||
m_author = authorBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
XtcError XtcParser::readPageTable() {
|
||||
if (m_header.pageTableOffset == 0) {
|
||||
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
||||
|
||||
@@ -67,8 +67,9 @@ class XtcParser {
|
||||
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
|
||||
size_t chunkSize = 1024);
|
||||
|
||||
// Get title from metadata
|
||||
// Get title/author from metadata
|
||||
std::string getTitle() const { return m_title; }
|
||||
std::string getAuthor() const { return m_author; }
|
||||
|
||||
bool hasChapters() const { return m_hasChapters; }
|
||||
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
|
||||
@@ -86,6 +87,7 @@ class XtcParser {
|
||||
std::vector<PageInfo> m_pageTable;
|
||||
std::vector<ChapterInfo> m_chapters;
|
||||
std::string m_title;
|
||||
std::string m_author;
|
||||
uint16_t m_defaultWidth;
|
||||
uint16_t m_defaultHeight;
|
||||
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
|
||||
@@ -96,6 +98,7 @@ class XtcParser {
|
||||
XtcError readHeader();
|
||||
XtcError readPageTable();
|
||||
XtcError readTitle();
|
||||
XtcError readAuthor();
|
||||
XtcError readChapters();
|
||||
};
|
||||
|
||||
|
||||
@@ -38,14 +38,16 @@ struct XtcHeader {
|
||||
uint8_t versionMajor; // 0x04: Format version major (typically 1) (together with minor = 1.0)
|
||||
uint8_t versionMinor; // 0x05: Format version minor (typically 0)
|
||||
uint16_t pageCount; // 0x06: Total page count
|
||||
uint32_t flags; // 0x08: Flags/reserved
|
||||
uint32_t headerSize; // 0x0C: Size of header section (typically 88)
|
||||
uint32_t reserved1; // 0x10: Reserved
|
||||
uint32_t tocOffset; // 0x14: TOC offset (0 if unused) - 4 bytes, not 8!
|
||||
uint8_t readDirection; // 0x08: Reading direction (0-2)
|
||||
uint8_t hasMetadata; // 0x09: Has metadata (0-1)
|
||||
uint8_t hasThumbnails; // 0x0A: Has thumbnails (0-1)
|
||||
uint8_t hasChapters; // 0x0B: Has chapters (0-1)
|
||||
uint32_t currentPage; // 0x0C: Current page (1-based) (0-65535)
|
||||
uint64_t metadataOffset; // 0x10: Metadata offset (0 if unused)
|
||||
uint64_t pageTableOffset; // 0x18: Page table offset
|
||||
uint64_t dataOffset; // 0x20: First page data offset
|
||||
uint64_t reserved2; // 0x28: Reserved
|
||||
uint32_t titleOffset; // 0x30: Title string offset
|
||||
uint64_t thumbOffset; // 0x28: Thumbnail offset
|
||||
uint32_t chapterOffset; // 0x30: Chapter data offset
|
||||
uint32_t padding; // 0x34: Padding to 56 bytes
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
+123
-11
@@ -4,6 +4,8 @@
|
||||
#include <SDCardManager.h>
|
||||
#include <miniz.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
|
||||
// Setup inflator
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
@@ -74,6 +76,10 @@ bool ZipFile::loadAllFileStatSlims() {
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
// Set cursor to start of central directory for sequential access
|
||||
lastCentralDirPos = zipDetails.centralDirOffset;
|
||||
lastCentralDirPosValid = true;
|
||||
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@@ -102,15 +108,35 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
// Phase 1: Try scanning from cursor position first
|
||||
uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset;
|
||||
uint32_t wrapPos = zipDetails.centralDirOffset;
|
||||
bool wrapped = false;
|
||||
bool found = false;
|
||||
|
||||
file.seek(startPos);
|
||||
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
bool found = false;
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) break; // End of list
|
||||
while (true) {
|
||||
uint32_t entryStart = file.position();
|
||||
|
||||
if (file.read(&sig, 4) != 4 || sig != 0x02014b50) {
|
||||
// End of central directory
|
||||
if (!wrapped && lastCentralDirPosValid && startPos != zipDetails.centralDirOffset) {
|
||||
// Wrap around to beginning
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
wrapped = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we've wrapped and reached our start position, stop
|
||||
if (wrapped && entryStart >= startPos) {
|
||||
break;
|
||||
}
|
||||
|
||||
file.seekCur(6);
|
||||
file.read(&fileStat->method, 2);
|
||||
@@ -123,15 +149,25 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
file.read(&fileStat->localHeaderOffset, 4);
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
if (strcmp(itemName, filename) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
if (nameLen < 256) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
if (strcmp(itemName, filename) == 0) {
|
||||
// Found it! Update cursor to next entry
|
||||
file.seekCur(m + k);
|
||||
lastCentralDirPos = file.position();
|
||||
lastCentralDirPosValid = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Name too long, skip it
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
// Skip the rest of this entry (extra field + comment)
|
||||
// Skip extra field + comment
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
@@ -253,6 +289,8 @@ bool ZipFile::close() {
|
||||
if (file) {
|
||||
file.close();
|
||||
}
|
||||
lastCentralDirPos = 0;
|
||||
lastCentralDirPosValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -266,6 +304,80 @@ bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes) {
|
||||
if (targets.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const bool wasOpen = isOpen();
|
||||
if (!wasOpen && !open()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!loadZipDetails()) {
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
|
||||
int matched = 0;
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) break;
|
||||
|
||||
file.seekCur(6);
|
||||
uint16_t method;
|
||||
file.read(&method, 2);
|
||||
file.seekCur(8);
|
||||
uint32_t compressedSize, uncompressedSize;
|
||||
file.read(&compressedSize, 4);
|
||||
file.read(&uncompressedSize, 4);
|
||||
uint16_t nameLen, m, k;
|
||||
file.read(&nameLen, 2);
|
||||
file.read(&m, 2);
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
uint32_t localHeaderOffset;
|
||||
file.read(&localHeaderOffset, 4);
|
||||
|
||||
if (nameLen < 256) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
uint64_t hash = fnvHash64(itemName, nameLen);
|
||||
SizeTarget key = {hash, nameLen, 0};
|
||||
|
||||
auto it = std::lower_bound(targets.begin(), targets.end(), key, [](const SizeTarget& a, const SizeTarget& b) {
|
||||
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||
});
|
||||
|
||||
while (it != targets.end() && it->hash == hash && it->len == nameLen) {
|
||||
if (it->index < sizes.size()) {
|
||||
sizes[it->index] = uncompressedSize;
|
||||
matched++;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) {
|
||||
const bool wasOpen = isOpen();
|
||||
if (!wasOpen && !open()) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user