Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6174cbc613 | ||
|
|
9d147cade9 | ||
|
|
792aab6d97 | ||
|
|
b58b51bfbe | ||
|
|
f42fab1c66 | ||
|
|
c9188a7347 | ||
|
|
e03aa16330 | ||
|
|
3319aa1720 | ||
|
|
3dfbc0383f | ||
|
|
64364bec5d | ||
|
|
556b8aee5b | ||
|
|
b1d1d757ba | ||
|
|
5ba1d5747f | ||
|
|
9fe4dc5e38 | ||
|
|
b1d037569b | ||
|
|
9737cb335c | ||
|
|
98118d6e24 | ||
|
|
fdffc2e5d9 | ||
|
|
a2db43d235 | ||
|
|
63093e606c | ||
|
|
a4ac3b2788 | ||
|
|
35a45f9c1e | ||
|
|
f3cad2f9e1 | ||
|
|
95d8cb712b | ||
|
|
b9867b0d09 | ||
|
|
3dac4446b7 | ||
|
|
3cc4069faa | ||
|
|
a3b8897977 | ||
|
|
f7f1a6f8bf | ||
|
|
7cad3cfb0e | ||
|
|
f180069643 | ||
|
|
a0ec5812b1 | ||
|
|
e84840b473 | ||
|
|
94ae750e89 | ||
|
|
8c5cd67f38 | ||
|
|
c5787d1cc1 | ||
|
|
e66de575a1 | ||
|
|
2a19eb9e0d | ||
|
|
fae3423dec | ||
|
|
932a472835 | ||
|
|
552b2683e6 | ||
|
|
bfa1d706b1 | ||
|
|
bae29fc6ba | ||
|
|
379f49d165 | ||
|
|
73ab9ab20d | ||
|
|
3a97f3e63e | ||
|
|
39ea4b045f | ||
|
|
444d87de82 | ||
|
|
859f6cb0d5 | ||
|
|
d8139e9605 | ||
|
|
b596f21e61 | ||
|
|
5fbc657aeb | ||
|
|
bb06219c9c | ||
|
|
5202bbf911 | ||
|
|
e0253a8664 | ||
|
|
e142e88ce7 | ||
|
|
6add0b91bc | ||
|
|
287457f7dd | ||
|
|
3e627112f6 | ||
|
|
1f5669a08a | ||
|
|
0dc92c142c | ||
|
|
4b34a576eb | ||
|
|
4fb843ef92 | ||
|
|
a43061305a | ||
|
|
7ec07b4d9d | ||
|
|
c2c1badc7e |
@@ -0,0 +1,13 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Scope or Roadmap Question (start here if unsure)
|
||||||
|
url: https://github.com/crosspoint-reader/crosspoint-reader/discussions
|
||||||
|
about: |
|
||||||
|
Not sure if your idea fits CrossPoint's scope? Start a Discussion before filing an issue.
|
||||||
|
See SCOPE.md and ROADMAP.md for what is in, out, and currently paused.
|
||||||
|
- name: Read the Scope document
|
||||||
|
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/SCOPE.md
|
||||||
|
about: The authoritative list of what CrossPoint will and will not accept.
|
||||||
|
- name: Read the Roadmap
|
||||||
|
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/ROADMAP.md
|
||||||
|
about: Current phase, what is being closed out, and what comes next.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Propose a new feature, enhancement, or change to CrossPoint
|
||||||
|
title: "Short, descriptive title of the request"
|
||||||
|
labels: ["enhancement", "needs-scope-review"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to propose a change to CrossPoint!
|
||||||
|
|
||||||
|
**Before you continue, please read [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).**
|
||||||
|
|
||||||
|
CrossPoint is intentionally narrow. Most rejected proposals are rejected for scope reasons that are already
|
||||||
|
documented. The checklist below exists to save both of us time.
|
||||||
|
|
||||||
|
If you are not sure whether your idea fits, open a [Discussion](../../discussions) first instead of filing
|
||||||
|
this issue.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: scope-check
|
||||||
|
attributes:
|
||||||
|
label: Scope Self-Check (required)
|
||||||
|
description: Please confirm each of the following. If any are unchecked, your issue will likely be closed.
|
||||||
|
options:
|
||||||
|
- label: I have read SCOPE.md and ROADMAP.md.
|
||||||
|
required: true
|
||||||
|
- label: This is **not** a new theme or theming change (themes are temporarily closed pending the move to SD-loaded themes).
|
||||||
|
required: true
|
||||||
|
- label: This is **not** a new external network connector (sync engine, cloud storage, remote file access, OPDS extensions beyond what exists, or any new "talk to a server" feature).
|
||||||
|
required: true
|
||||||
|
- label: This is **not** an interactive app (game, calculator, notepad), writing/authoring tool, RSS/news/browser feature, media playback feature, or PDF rendering.
|
||||||
|
required: true
|
||||||
|
- label: The stock firmware does **not** already handle this well.
|
||||||
|
required: true
|
||||||
|
- label: No other popular CrossPoint fork already handles this well (or, if one does, I explain below why CrossPoint still needs it).
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem this solves
|
||||||
|
description: What user-facing problem or reading-experience gap does this address? Be concrete.
|
||||||
|
placeholder: e.g., "When reading in landscape, paragraph breaks are inconsistent because..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: stock-gap
|
||||||
|
attributes:
|
||||||
|
label: Why the stock firmware (and other forks) do not already solve this
|
||||||
|
description: Explain which existing solutions you checked and why they fall short. This is the core scope filter.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposal
|
||||||
|
attributes:
|
||||||
|
label: Proposed change
|
||||||
|
description: A short description of what you would build or change. Focus on user impact; implementation details can come later.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: tradeoffs
|
||||||
|
attributes:
|
||||||
|
label: Memory / flash / complexity cost
|
||||||
|
description: |
|
||||||
|
CrossPoint runs on 380KB of RAM. Roughly how much DRAM, flash, or code complexity does this add?
|
||||||
|
"Don't know" is a valid answer, but please attempt an estimate.
|
||||||
|
placeholder: e.g., "Adds ~2KB flash for the new font tables, no DRAM impact at runtime."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Anything else relevant (links, screenshots, related discussions).
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -3,16 +3,34 @@
|
|||||||
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
|
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
|
||||||
* **What changes are included?**
|
* **What changes are included?**
|
||||||
|
|
||||||
|
## Scope Check
|
||||||
|
|
||||||
|
CrossPoint is intentionally narrow. See [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).
|
||||||
|
Please confirm:
|
||||||
|
|
||||||
|
- [ ] I have read SCOPE.md and ROADMAP.md.
|
||||||
|
- [ ] This PR is **not** a new built-in theme (themes are temporarily closed pending the move to SD-loaded themes).
|
||||||
|
- [ ] This PR is **not** a new external network connector (sync engine, cloud storage, remote file access, etc.).
|
||||||
|
- [ ] This PR is **not** an interactive app, writing tool, RSS/news/browser, media playback, or PDF feature.
|
||||||
|
- [ ] The stock firmware does not already handle this well, **and** no other popular CrossPoint fork already does
|
||||||
|
(or, if one does, I explain why CrossPoint still needs it below).
|
||||||
|
- [ ] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or recovery code, I have coordinated with
|
||||||
|
the relevant maintainer.
|
||||||
|
|
||||||
|
**If this PR was opened against the previous (broader) scope and was already in flight under Phase 0, link the
|
||||||
|
relevant Discussion or issue so reviewers can see the history.**
|
||||||
|
|
||||||
## Additional Context
|
## Additional Context
|
||||||
|
|
||||||
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
|
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
|
||||||
specific areas to focus on).
|
specific areas to focus on).
|
||||||
|
* Memory / flash impact, if known.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### AI Usage
|
### AI Usage
|
||||||
|
|
||||||
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
|
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
|
||||||
helps set the right context for reviewers.
|
helps set the right context for reviewers.
|
||||||
|
|
||||||
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO >**_
|
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO >**_
|
||||||
|
|||||||
@@ -91,9 +91,13 @@ jobs:
|
|||||||
restore-keys: pio-${{ runner.os }}-
|
restore-keys: pio-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Build CrossPoint
|
- name: Build CrossPoint
|
||||||
|
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
|
||||||
|
# .pio/build tree (project-checksum mismatch after the S3 toolchain
|
||||||
|
# installs into the restored cache), deleting the default firmware.bin
|
||||||
|
# before the artifact upload.
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
pio run | tee pio.log
|
pio run -e default -e sticky | tee pio.log
|
||||||
|
|
||||||
- name: Extract firmware stats
|
- name: Extract firmware stats
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
open-x4-sdk
|
||||||
|
fs_
|
||||||
lib/EpdFont/fontsrc
|
lib/EpdFont/fontsrc
|
||||||
lib/I18n/I18nKeys.h
|
lib/I18n/I18nKeys.h
|
||||||
lib/I18n/I18nStrings.h
|
lib/I18n/I18nStrings.h
|
||||||
@@ -23,3 +25,10 @@ lib/EpdFont/scripts/output/
|
|||||||
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
||||||
.claude/*
|
.claude/*
|
||||||
!.claude/skills/
|
!.claude/skills/
|
||||||
|
/managed_components
|
||||||
|
/.dummy
|
||||||
|
/CMakeLists.txt
|
||||||
|
/dependencies.lock
|
||||||
|
/sdkconfig.default
|
||||||
|
/sdkconfig.defaults
|
||||||
|
sdkconfig.sticky
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
|||||||
|
|
||||||
## What can CrossPoint do?
|
## What can CrossPoint do?
|
||||||
|
|
||||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, dictionary lookups ([StarDict](docs/dictionary.md)), go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||||
|
|
||||||
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
||||||
|
|
||||||
@@ -42,8 +42,6 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
|||||||
|
|
||||||
### Coming soon:
|
### Coming soon:
|
||||||
|
|
||||||
- Dictionary lookup — inline word lookup without leaving the reader.
|
|
||||||
|
|
||||||
- More themes.
|
- More themes.
|
||||||
|
|
||||||
- Much more! stay tuned.
|
- Much more! stay tuned.
|
||||||
@@ -138,6 +136,7 @@ Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py`
|
|||||||
- [Web server endpoints](./docs/webserver-endpoints.md)
|
- [Web server endpoints](./docs/webserver-endpoints.md)
|
||||||
- [Project scope](./SCOPE.md)
|
- [Project scope](./SCOPE.md)
|
||||||
- [Contributing docs](./docs/contributing/README.md)
|
- [Contributing docs](./docs/contributing/README.md)
|
||||||
|
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - FreeInkUI components for new screens, the touch bridge for existing ones, and build envs for the non-Xteink touch devices
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,6 +159,24 @@ cd crosspoint-reader
|
|||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nix/NixOS
|
||||||
|
|
||||||
|
Nix/NixOS users can enter the development shell with either `nix develop` (flakes) or `nix-shell`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop -f nix
|
||||||
|
# or
|
||||||
|
nix-shell nix
|
||||||
|
```
|
||||||
|
|
||||||
|
To flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.udev.packages = with pkgs; [ platformio-core.udev ];
|
||||||
|
```
|
||||||
|
|
||||||
|
After rebuilding the system configuration, reconnect the device or reload udev rules.
|
||||||
|
|
||||||
### Build / flash / monitor
|
### Build / flash / monitor
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
# CrossPoint Reader Roadmap
|
||||||
|
|
||||||
|
This roadmap describes how CrossPoint is moving through the tighter scope defined in [SCOPE.md](SCOPE.md). It is
|
||||||
|
intentionally phased: Phase 0 closed out the commitments already in flight before locking down to the stricter
|
||||||
|
"fill gaps the stock firmware leaves" delineator.
|
||||||
|
|
||||||
|
Phases are sequential. We do not start the next phase until the prior one is wrapped or explicitly carried over.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 - Close Out Legacy Scope Items — **COMPLETE**
|
||||||
|
|
||||||
|
**Goal:** Land the work that was already in motion under the prior, broader scope so contributors are not left
|
||||||
|
hanging, and so we enter the stricter phases with a clean slate.
|
||||||
|
|
||||||
|
**Landed in Phase 0:**
|
||||||
|
|
||||||
|
* **RTL support PRs.** The in-flight right-to-left work was reviewed, iterated, and merged.
|
||||||
|
* **Dictionary PR.** The offline dictionary lookup work was reviewed and merged.
|
||||||
|
* **Bookmarks** feature. First-class navigation markers in EPUBs.
|
||||||
|
* ~~**Transparent sleep screens.**~~ Shelved; not picked back up under the stricter phases.
|
||||||
|
|
||||||
|
Phase 0 is closed. The tighter scope in [SCOPE.md](SCOPE.md) is now fully enforced. "But it was on the old roadmap"
|
||||||
|
is not a valid argument for accepting a PR.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 - Consolidation, Footprint, and Multi-Device Support — **IN PROGRESS**
|
||||||
|
|
||||||
|
**Goal:** Reduce memory and flash usage, clean up the codebase, and land the SDK / HAL generalization work so
|
||||||
|
CrossPoint runs cleanly on ESP32-based e-reader hardware beyond Xteink (X3 / X4), including ESP32-S3 class devices.
|
||||||
|
|
||||||
|
**Focus areas:**
|
||||||
|
|
||||||
|
* DRAM and heap fragmentation reduction across the reader core.
|
||||||
|
* Flash footprint reduction (dead code, redundant strings, oversized tables).
|
||||||
|
* Refactors that tighten the HAL / SDK boundary.
|
||||||
|
* Pluggable per-device SDK layers (display, input, storage, battery) and per-device build configuration without
|
||||||
|
forking the reader core.
|
||||||
|
* Documentation for adding a new ESP32 e-reader target.
|
||||||
|
* E-ink driver refinement (ghosting, partial update behavior).
|
||||||
|
|
||||||
|
**Closed during this phase:** new themes built into firmware, new external network connectors (sync engines, cloud
|
||||||
|
storage, remote file access).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 - Languages, Fonts, and Themes
|
||||||
|
|
||||||
|
**Goal:** With the codebase smaller and portable, make reading great in every language: multi-language support,
|
||||||
|
better font support with custom fonts, UI translations, and themes loaded from the SD card instead of consuming
|
||||||
|
flash.
|
||||||
|
|
||||||
|
**Focus areas:**
|
||||||
|
|
||||||
|
* Multi-language reading support (underserved languages, complex script support where realistic on ESP32 hardware).
|
||||||
|
* Better font support and custom fonts.
|
||||||
|
* UI languages and localization.
|
||||||
|
* Moving themes off-firmware to SD-loaded assets (see SCOPE.md Section 6).
|
||||||
|
* **Moving hyphenation files off-firmware.** Hyphenation rules vary per language and the files are large (German
|
||||||
|
alone is ~200KB). Today these eat flash budget that should be available for the reader core. The plan is to build
|
||||||
|
a downloader analogous to the existing font downloader and store the dictionaries on SD / SPIFFS, loading on
|
||||||
|
demand. This unlocks better hyphenation for long-word languages (German, Finnish, Norwegian, etc.) without paying
|
||||||
|
the flash cost up front.
|
||||||
|
|
||||||
|
This phase depends on Phase 1 cleanup landing first; otherwise we generalize a moving target.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of Roadmap
|
||||||
|
|
||||||
|
The following are explicitly *not* on the roadmap. They may live in other CrossPoint forks; they will not be picked
|
||||||
|
up here:
|
||||||
|
|
||||||
|
* Interactive apps (games, calculators, notepads).
|
||||||
|
* Writing / authoring tools.
|
||||||
|
* Active connectivity features (RSS, news, browsers).
|
||||||
|
* PDF rendering as a first-class format.
|
||||||
|
|
||||||
|
See [SCOPE.md](SCOPE.md) for the full rationale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How This Roadmap Changes
|
||||||
|
|
||||||
|
* Phase boundaries are decided by maintainers, not by individual PRs.
|
||||||
|
* If a phase needs to be extended or an item carried over, that is documented here with a short note.
|
||||||
|
* Proposals for new phases or reordering should go through a Discussion first.
|
||||||
@@ -1,62 +1,174 @@
|
|||||||
# Project Vision & Scope: CrossPoint Reader
|
# 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
|
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for ESP32-based e-reader
|
||||||
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
devices. Xteink hardware (X3, X4) is where the project started and remains a primary target, but CrossPoint is
|
||||||
|
explicitly broadening to support the wider ecosystem of small ESP32 e-ink readers. We believe a dedicated e-reader
|
||||||
|
should do one thing exceptionally well: **facilitate focused reading.**
|
||||||
|
|
||||||
## 1. Core Mission
|
## 1. Core Mission
|
||||||
|
|
||||||
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
|
To provide a lightweight, high-performance firmware that maximizes the potential of ESP32-based e-reader hardware,
|
||||||
usability over "swiss-army-knife" functionality.
|
prioritizing legibility, performance, and usability over "swiss-army-knife" functionality.
|
||||||
|
|
||||||
## 2. Scope
|
CrossPoint is **not** a kitchen-sink firmware, and it is **not** Xteink-only. We want clean, maintainable code that
|
||||||
|
the community can build on, and that runs across the range of ESP32 e-reader devices (ESP32-C3, ESP32-S3, and
|
||||||
|
adjacent variants). Every accepted change should make that goal easier, not harder. Device-specific code should live
|
||||||
|
behind the HAL / SDK boundary so the reader core stays portable.
|
||||||
|
|
||||||
|
## 2. Guiding Principle: Fill Gaps the Stock Firmware Leaves
|
||||||
|
|
||||||
|
CrossPoint exists to do the things the stock firmware does poorly or not at all. New work is evaluated against that
|
||||||
|
delineator:
|
||||||
|
|
||||||
|
* **Does the stock firmware already do this well?** If yes, we will not duplicate it.
|
||||||
|
* **Is another popular CrossPoint fork already solving this well?** If yes, we generally defer to that fork rather than
|
||||||
|
fragmenting the ecosystem.
|
||||||
|
* **Does this directly improve the reading experience or the firmware's long-term maintainability?** If no, it is out
|
||||||
|
of scope.
|
||||||
|
|
||||||
|
### Language Priority
|
||||||
|
|
||||||
|
English is the priority language for new features and UI work. Other languages are considered, with preference given
|
||||||
|
to cases where the stock firmware fails (rendering, input, fonts, layout) or where existing CrossPoint forks have not
|
||||||
|
addressed the gap.
|
||||||
|
|
||||||
|
## 3. Current Focus (Until Further Notice)
|
||||||
|
|
||||||
|
We are intentionally narrowing scope to consolidate the codebase as we open it up to more ESP32 e-reader devices.
|
||||||
|
During this period, the priorities are:
|
||||||
|
|
||||||
|
* **Memory footprint:** Reducing DRAM usage and heap fragmentation. The ESP32-C3 is the tightest target and sets the
|
||||||
|
ceiling, but the gains benefit every ESP32 variant we run on.
|
||||||
|
* **Flash footprint:** Trimming binary size to leave room for additional device targets and features.
|
||||||
|
* **Code cleanup:** Refactoring, removing dead code, tightening abstractions, and improving readability.
|
||||||
|
* **Reading experience:** EPUB parsing and rendering, typography, hyphenation, line spacing, font handling, and
|
||||||
|
legibility improvements.
|
||||||
|
|
||||||
|
### Temporarily Closed Areas
|
||||||
|
|
||||||
|
PRs in the following areas will be closed until this notice is lifted. Adding these now makes the cleanup and
|
||||||
|
multi-device work materially harder:
|
||||||
|
|
||||||
|
* **New themes.** The existing theming surface is frozen.
|
||||||
|
* **New external network connectors.** This includes sync engines, cloud storage clients, OPDS extensions beyond what
|
||||||
|
exists, remote file access, and any new "talk to a server" feature.
|
||||||
|
|
||||||
|
If you are unsure whether your idea falls into one of these categories, open a Discussion first.
|
||||||
|
|
||||||
|
## 4. Scope
|
||||||
|
|
||||||
### In-Scope
|
### In-Scope
|
||||||
|
|
||||||
*These are features that directly improve the primary purpose of the device.*
|
*Features that directly improve the core reading experience or the firmware's maintainability.*
|
||||||
|
|
||||||
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
|
* **EPUB Rendering & Optimization:** Improvements to the rendering engine, CSS/image handling, and parsing
|
||||||
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
|
performance.
|
||||||
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
|
* **Typography & Legibility:** Custom font support, hyphenation, line and paragraph spacing, margins.
|
||||||
engine.
|
* **E-Ink Driver Refinement:** Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||||
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
|
* **Reading UX:** Bookmarks, progress tracking, button mapping, page navigation, and other in-reader interactions.
|
||||||
capabilities.
|
* **Library Management:** Simple, intuitive ways to organize and navigate a local book collection.
|
||||||
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
|
* **Local Transfer:** Simple, pull-based loading via the existing web server or widely used standards. New connectors
|
||||||
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
|
are currently paused (see Section 3).
|
||||||
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
* **Reference Tools:** Local, offline dictionary lookup.
|
||||||
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
* **Memory, Flash, and Code Quality:** Refactors and cleanups that reduce resource use or improve maintainability,
|
||||||
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
even without a user-visible feature.
|
||||||
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
|
||||||
without breaking focus.
|
|
||||||
* **Clock Display (device dependent):**
|
|
||||||
|
|
||||||
| Device | Scope |
|
|
||||||
| -- | -- |
|
|
||||||
| X3 | The X3 uses a dedicated DS3231 RTC, which maintains accurate time across sleep cycles and can be treated as a reliable wall clock. |
|
|
||||||
| X4 | The X4 relies on the ESP32-C3's internal RTC, which drifts significantly during deep sleep. NTP sync could correct this, with an appropriate user experience around connecting to the internet on wake or on demand. This causes some tension with the **Active Connectivity** section below, so please open a discussion about this UX if it's a feature you would find useful. |
|
|
||||||
|
|
||||||
### Out-of-Scope
|
### Out-of-Scope
|
||||||
|
|
||||||
*These items are rejected because they compromise the device's stability or mission.*
|
*Rejected because they compromise the device's stability, maintainability, or core mission.*
|
||||||
|
|
||||||
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
|
* **Interactive Apps:** No notepads, calculators, or games. These belong in other forks and are not part of
|
||||||
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
CrossPoint's focus.
|
||||||
and complicate the single-core CPU's execution.
|
* **Writing / Authoring Tools:** No typed notes, journals, or editors. Input hardware and RAM are wrong for this, and
|
||||||
* **Media Playback:** No Audio players or Audio-books.
|
other forks already explore this space.
|
||||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
* **Active Connectivity:** No RSS readers, news aggregators, or web browsers. Background Wi-Fi drains the battery and
|
||||||
capabilities and more powerful chips.
|
complicates the single-core CPU.
|
||||||
|
* **Media Playback:** No audio players or audiobooks.
|
||||||
|
* **Complex Annotation:** No typed-out notes.
|
||||||
|
* **Duplication of stock firmware behavior** that already works well.
|
||||||
|
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather
|
||||||
|
than reflowable text, resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
||||||
|
Out of scope on the current hardware class.
|
||||||
|
|
||||||
### In-scope — Technically Unsupported
|
## 5. Idea Evaluation
|
||||||
|
|
||||||
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
|
Before proposing a feature, ask:
|
||||||
|
|
||||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
1. Does the stock firmware already handle this well? We should hit that bar or surpass it.
|
||||||
|
2. Does another popular CrossPoint fork already handle this well? If yes, we usually defer.
|
||||||
|
3. Does it improve the core reading experience, or reduce memory / flash / code complexity?
|
||||||
|
4. Is it in one of the temporarily closed areas (new themes, new network connectors)? If yes, wait.
|
||||||
|
5. Would adding it make the codebase harder to clean up or harder to port to other devices? If yes, rework or defer.
|
||||||
|
|
||||||
## 3. Idea Evaluation
|
If the idea passes those filters, it is a candidate. If you are not sure, open a **Discussion** before writing any
|
||||||
|
code.
|
||||||
|
|
||||||
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
|
> **Note to Contributors:** CrossPoint is intentionally narrow. "It would be cool if..." features are not enough; the
|
||||||
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
|
> bar is "this fixes something the stock firmware does poorly, or it makes the firmware leaner and easier to maintain."
|
||||||
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
|
## 6. Calls to Action
|
||||||
> coding!
|
|
||||||
|
These are the areas where contributor help is most valuable right now. If you want to take one of these on, open a
|
||||||
|
Discussion or issue first so we can coordinate.
|
||||||
|
|
||||||
|
### Theme System: Move Themes Off-Firmware
|
||||||
|
|
||||||
|
We want to abstract themes out of the firmware entirely so they no longer consume flash, and instead load from the SD
|
||||||
|
card. This directly supports the current focus on flash footprint and code cleanup.
|
||||||
|
|
||||||
|
* **Status:** [@itsthisjustin](https://github.com/itsthisjustin) plans to take this on eventually but is very open to
|
||||||
|
someone else claiming it sooner.
|
||||||
|
* **Why it matters:** Every built-in theme costs flash that we would rather spend on rendering, fonts, or future
|
||||||
|
device support. SD-loaded themes also let users customize without rebuilding firmware.
|
||||||
|
* **How to claim:** Comment on the relevant Discussion (or open one) before starting.
|
||||||
|
|
||||||
|
### SDK Abstraction: Generalize the Lower Layers for All ESP32 E-Readers
|
||||||
|
|
||||||
|
To support the broader range of ESP32-based e-reader hardware, the SDK layers (display, input, storage, battery,
|
||||||
|
etc.) need to be abstracted away from Xteink-specific assumptions and made pluggable per device.
|
||||||
|
|
||||||
|
* **Status:** [@itsthisjustin](https://github.com/itsthisjustin) is actively working on this.
|
||||||
|
* **How to help:** Coordinate with itsthisjustin before opening PRs that touch `freeink-sdk/` or `lib/hal/` so the
|
||||||
|
work does not collide. Smaller adjacent cleanups (removing hardcoded assumptions, tightening HAL boundaries) are
|
||||||
|
welcome if scoped against the in-flight direction.
|
||||||
|
|
||||||
|
### Identifying Other Stock-Firmware Gaps
|
||||||
|
|
||||||
|
We want help cataloguing things the stock firmware (and other popular CrossPoint forks) handle poorly or not at all,
|
||||||
|
so future work has a clear target list. Particularly interested in:
|
||||||
|
|
||||||
|
* **RTL (right-to-left) text support:** Arabic, Hebrew, Persian, and similar scripts.
|
||||||
|
* **Languages with poor stock and fork coverage:** Especially those that need shaping, complex layout, or
|
||||||
|
non-Latin font work that nobody is handling well today.
|
||||||
|
* **Other gaps:** Rendering edge cases, accessibility issues, input quirks, anything stock does badly and existing
|
||||||
|
forks have not fixed.
|
||||||
|
|
||||||
|
* **Status:** [@uxjulia](https://github.com/uxjulia) is coordinating this effort.
|
||||||
|
|
||||||
|
If you can read or use the device in one of these languages, your feedback (even without code) is genuinely useful.
|
||||||
|
Open a Discussion with concrete examples (screenshots, sample EPUBs, expected vs actual behavior) and we will
|
||||||
|
prioritize from there.
|
||||||
|
|
||||||
|
## 7. Funding and Contributor Sustainability
|
||||||
|
|
||||||
|
CrossPoint uses [Royalty.dev](https://royalty.dev) (yes, a product built by [@itsthisjustin](https://github.com/itsthisjustin))
|
||||||
|
to fund contributors. There has been some tension in the community around this, so the intent is being clarified
|
||||||
|
here directly.
|
||||||
|
|
||||||
|
**Why we do this:**
|
||||||
|
|
||||||
|
* To maintain long-term interest from contributors and maintainers, in direct response to substantial community
|
||||||
|
requests for a way to give back.
|
||||||
|
* To motivate contributors to invest in the *core* project rather than spinning up competing forks.
|
||||||
|
* To help pay for new ESP32 devices so we can port CrossPoint to additional hardware.
|
||||||
|
* To give the project a credible long-term path to sustainability.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
|
||||||
|
* Funds are distributed automatically to contributors based on impact to the codebase and tenure on the project.
|
||||||
|
* Over **$600** was raised in the first few days after opening up funding, which is a signal the demand is real.
|
||||||
|
* The exact scoring methodology is published at <https://app.royalty.dev/transparency>.
|
||||||
|
|
||||||
|
**This is not fixed in stone.** The weighting, eligibility, and distribution rules can be tweaked as we learn what
|
||||||
|
works for this project. If you have concerns or suggestions about how funds are allocated, open a Discussion. The
|
||||||
|
goal is a system that fairly recognizes the people doing the work, not a perfect one on day one.
|
||||||
|
|||||||
+47
-30
@@ -28,8 +28,10 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
|||||||
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
||||||
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
||||||
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
||||||
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
|
- [Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default)
|
||||||
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
|
- [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks)
|
||||||
|
- [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose)
|
||||||
|
- [Syncing While Reading](#syncing-while-reading)
|
||||||
- [3.7 Sleep Screen](#37-sleep-screen)
|
- [3.7 Sleep Screen](#37-sleep-screen)
|
||||||
- [Cover settings](#cover-settings)
|
- [Cover settings](#cover-settings)
|
||||||
- [Custom images](#custom-images)
|
- [Custom images](#custom-images)
|
||||||
@@ -261,6 +263,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- "ON" - Vertical space will be added between paragraphs in Reading Mode
|
- "ON" - Vertical space will be added between paragraphs in Reading Mode
|
||||||
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
|
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
|
||||||
|
|
||||||
|
- **Dictionary**: Select the StarDict dictionary used for word lookups while reading, or "None" to disable lookups. *(Only shown when at least one dictionary folder exists under `/dictionaries/` on the SD card — see [docs/dictionary.md](docs/dictionary.md) for setup and usage.)*
|
||||||
|
|
||||||
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||||
|
|
||||||
- **Images**: Whether to display embedded images (JPG/PNG) found in EPUB files; options are "ON" (default) or "OFF".
|
- **Images**: Whether to display embedded images (JPG/PNG) found in EPUB files; options are "ON" (default) or "OFF".
|
||||||
@@ -280,6 +284,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
|
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
|
||||||
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
|
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
|
||||||
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
|
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
|
||||||
|
- "Dictionary" - Hold Confirm (~0.4 second) to start dictionary word selection on the current page (see [docs/dictionary.md](docs/dictionary.md)).
|
||||||
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
|
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
|
||||||
|
|
||||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||||
@@ -297,7 +302,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
|
|
||||||
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
|
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
|
||||||
|
|
||||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
|
- **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation.
|
||||||
|
|
||||||
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
|
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
|
||||||
|
|
||||||
@@ -360,9 +365,37 @@ Behavior notes:
|
|||||||
CrossPoint can sync reading progress with KOReader-compatible sync servers.
|
CrossPoint can sync reading progress with KOReader-compatible sync servers.
|
||||||
It also interoperates with KOReader apps/devices when they use the same server and credentials.
|
It also interoperates with KOReader apps/devices when they use the same server and credentials.
|
||||||
|
|
||||||
##### Option A: Free Public Server (`sync.koreader.rocks`)
|
##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)
|
||||||
|
|
||||||
1. Register a user once (only if needed):
|
When **Sync Server URL** is left empty, CrossPoint uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossPoint-to-CrossPoint sync.
|
||||||
|
|
||||||
|
1. On each CrossPoint device:
|
||||||
|
|
||||||
|
- Go to **Settings -> System -> KOReader Sync**.
|
||||||
|
|
||||||
|
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||||
|
|
||||||
|
- Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`).
|
||||||
|
|
||||||
|
- On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**.
|
||||||
|
|
||||||
|
Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server.
|
||||||
|
|
||||||
|
##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)
|
||||||
|
|
||||||
|
Use this if you already sync KOReader devices against the official public server.
|
||||||
|
|
||||||
|
1. On each CrossPoint device:
|
||||||
|
|
||||||
|
- Go to **Settings -> System -> KOReader Sync**.
|
||||||
|
|
||||||
|
- Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead).
|
||||||
|
|
||||||
|
- Set **Username** and **Password** to your existing KOReader Sync credentials.
|
||||||
|
|
||||||
|
- Run **Authenticate**.
|
||||||
|
|
||||||
|
2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
USERNAME="user"
|
USERNAME="user"
|
||||||
@@ -375,27 +408,9 @@ curl -i "https://sync.koreader.rocks/users/create" \
|
|||||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||||
```
|
```
|
||||||
|
|
||||||
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
|
|
||||||
|
|
||||||
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
|
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
|
||||||
|
|
||||||
2. On each CrossPoint device:
|
##### Option C: Self-Hosted Server (Docker Compose)
|
||||||
|
|
||||||
- Go to **Settings -> System -> KOReader Sync**.
|
|
||||||
|
|
||||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
|
||||||
|
|
||||||
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
|
|
||||||
|
|
||||||
- Run **Authenticate**.
|
|
||||||
|
|
||||||
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
|
||||||
|
|
||||||
- Choose **Apply Remote** to jump to remote progress.
|
|
||||||
|
|
||||||
- Choose **Upload Local** to push current progress.
|
|
||||||
|
|
||||||
##### Option B: Self-Hosted Server (Docker Compose)
|
|
||||||
|
|
||||||
1. Start a sync server:
|
1. Start a sync server:
|
||||||
|
|
||||||
@@ -468,11 +483,12 @@ If this returns `HTTP 402` with `{"code":2002,"message":"Username is already reg
|
|||||||
|
|
||||||
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
|
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
|
||||||
|
|
||||||
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
##### Syncing While Reading
|
||||||
|
|
||||||
- Choose **Apply Remote** to jump to remote progress.
|
Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
|
||||||
|
|
||||||
- Choose **Upload Local** to push current progress.
|
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
|
||||||
|
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
|
||||||
|
|
||||||
### 3.7 Sleep Screen
|
### 3.7 Sleep Screen
|
||||||
|
|
||||||
@@ -570,7 +586,7 @@ If the device goes to sleep or you close the book while viewing a footnote, the
|
|||||||
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||||
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
||||||
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
|
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
|
||||||
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Disabled" does nothing. A short press always opens the Reader Menu.
|
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Dictionary" starts a word lookup, "Disabled" does nothing. A short press always opens the Reader Menu.
|
||||||
|
|
||||||
### Supported Languages
|
### Supported Languages
|
||||||
|
|
||||||
@@ -592,6 +608,7 @@ Available options include:
|
|||||||
|
|
||||||
- **Select Chapter** – Open the table of contents to jump to a specific chapter (see [Chapter Selection](#51-chapter-selection) below).
|
- **Select Chapter** – Open the table of contents to jump to a specific chapter (see [Chapter Selection](#51-chapter-selection) below).
|
||||||
- **Footnotes** – Navigate to the footnotes for the current section *(only shown in books that contain footnotes)*.
|
- **Footnotes** – Navigate to the footnotes for the current section *(only shown in books that contain footnotes)*.
|
||||||
|
- **Look Up** – Select a word on the current page and show its dictionary definition (see [docs/dictionary.md](docs/dictionary.md)). Requires a dictionary to be selected in **Settings → Reader → Dictionary**.
|
||||||
- **Reading Orientation** – Cycle through screen orientations without leaving the reader.
|
- **Reading Orientation** – Cycle through screen orientations without leaving the reader.
|
||||||
- **Auto Turn (Pages Per Minute)** – Cycle through automatic page turn speed options for hands-free reading.
|
- **Auto Turn (Pages Per Minute)** – Cycle through automatic page turn speed options for hands-free reading.
|
||||||
- **Go to %** – Jump to a specific position in the book by percentage.
|
- **Go to %** – Jump to a specific position in the book by percentage.
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
|||||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||||
| grep -v -E '^lib/uzlib/' \
|
| grep -v -E '^lib/uzlib/' \
|
||||||
|
| grep -v -E '^lib/miniz/third_party/' \
|
||||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||||
# Restore strict pipeline failure handling for the rest of the script.
|
# Restore strict pipeline failure handling for the rest of the script.
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ It is written for software developers who may be new to embedded development.
|
|||||||
- [Architecture Overview](./architecture.md)
|
- [Architecture Overview](./architecture.md)
|
||||||
- [Development Workflow](./development-workflow.md)
|
- [Development Workflow](./development-workflow.md)
|
||||||
- [Testing and Debugging](./testing-debugging.md)
|
- [Testing and Debugging](./testing-debugging.md)
|
||||||
|
- [Touch and UI Development](./touch-and-ui.md)
|
||||||
|
|
||||||
If you are new, start with [Getting Started](./getting-started.md).
|
If you are new, start with [Getting Started](./getting-started.md).
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# Touch and UI Development
|
||||||
|
|
||||||
|
CrossPoint now runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. There are two supported ways to get there:
|
||||||
|
|
||||||
|
1. **New screens: build them with FreeInkUI components.** Touch hit-testing, tap highlighting, long-press, and button focus navigation come with the component; you never hand-roll coordinate math.
|
||||||
|
2. **Existing screens and in-flight features: use the MappedInputManager touch bridge.** A small set of helpers adds tap/hold/swipe support to hand-rolled rendering without restructuring the activity.
|
||||||
|
|
||||||
|
If you are starting a new activity, use FreeInkUI. If you have a feature branch with a hand-rolled screen already working on buttons, use the bridge; do not rewrite mid-flight.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path 1: New screens with FreeInkUI
|
||||||
|
|
||||||
|
FreeInkUI (`freeink-sdk/libs/ui/FreeInkUI`, namespace `freeink::ui`) is an immediate-mode component library. The core idea:
|
||||||
|
|
||||||
|
- While a component renders, it registers its tappable areas ("hit rects") into the frame's interaction buffer via `Frame::hit(rect, action, value, inputMask, state)`.
|
||||||
|
- Each loop you build an `InputSnapshot` from the input manager and route it against that buffer. If a tap (or mapped button press) lands in a registered rect, the routed `ActionEvent` tells you which action fired and with what value.
|
||||||
|
- Components query `frame.stateFor(action, value)` while painting, so touch-down highlight and focus states render correctly without any per-activity code.
|
||||||
|
|
||||||
|
You get touch, hold highlighting, long-press, minimum touch-target sizing, and orientation-aware coordinates for free. The same action IDs fire from physical buttons, so one code path serves both input styles.
|
||||||
|
|
||||||
|
### Component inventory
|
||||||
|
|
||||||
|
All under `freeink-sdk/libs/ui/FreeInkUI/include/components/`:
|
||||||
|
|
||||||
|
| Category | Components |
|
||||||
|
|---|---|
|
||||||
|
| Controls | `button`, `checkbox`, `slider`, `progress-bar`, `header` |
|
||||||
|
| Lists | `list` (virtualized), `table`, `dropdown`, `radio-group`, `setting-row`, `toggle-row`, `stepper-row` |
|
||||||
|
| Keyboard | `keyboard` (QWERTY/AZERTY/QWERTZ/ES layouts), `key-grid` |
|
||||||
|
| Overlays | `popup`, `option-dialog`, `context-menu`, `message-panel`, `toast` |
|
||||||
|
| Bars | `status-bar`, `tab-bar`, `reader-chrome`, `battery-indicator`, `gesture-bar`, `tap-zones` |
|
||||||
|
| Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` |
|
||||||
|
| Text | `text-field`, `text-area` |
|
||||||
|
|
||||||
|
### Integration skeleton
|
||||||
|
|
||||||
|
The in-tree reference is [`src/activities/util/KeyboardEntryActivity.cpp`](../../src/activities/util/KeyboardEntryActivity.cpp); it drives the FreeInkUI keyboard component inside a normal Activity. The shape:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <FreeInkUIGfxRenderer.h>
|
||||||
|
namespace fui = freeink::ui;
|
||||||
|
|
||||||
|
// Render step: draw the component and (re)register its hit rects.
|
||||||
|
fui::GfxRendererTarget target(renderer); // adapts GfxRenderer to FreeInkUI's DrawTarget
|
||||||
|
target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID);
|
||||||
|
const fui::DeviceContext device = target.deviceContext(); // orientation, safe area, touch sizing
|
||||||
|
|
||||||
|
fui::Frame<48> frame(target, device, fui::InputSnapshot{}, interactions); // 48 = max hit rects
|
||||||
|
fui::KeyboardProps props;
|
||||||
|
props.layout = ¤tLayout();
|
||||||
|
props.keyAction = ACTION_KEY; // one action id; the key is the event value
|
||||||
|
props.inputMask = fui::InputTouch | fui::InputLongPress;
|
||||||
|
fui::keyboard(frame, kbRect, props); // draws AND registers hit rects
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Input step (each loop): feed touch state, act on routed events.
|
||||||
|
int tx, ty, tapX, tapY, hx, hy;
|
||||||
|
const bool pressedDown = mappedInput.wasScreenTouchDown(tx, ty);
|
||||||
|
const bool tapped = mappedInput.wasScreenTapped(tapX, tapY);
|
||||||
|
const bool inContact = mappedInput.isScreenTouchHeld(hx, hy);
|
||||||
|
|
||||||
|
const auto result = touchRouter.update(interactions, pressedDown, tx, ty,
|
||||||
|
tapped, tapX, tapY, inContact, millis());
|
||||||
|
if (result.event) {
|
||||||
|
activateValue(result.event.value, result.event.longPress);
|
||||||
|
requestUpdate();
|
||||||
|
} else if (result.activeChanged) {
|
||||||
|
requestUpdate(); // repaint touch-down highlight
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `Frame<N>` is templated on the max number of hit rects; the buffer is stack/static, no heap.
|
||||||
|
- `fui::TouchHoldRouter` (see `components/keyboard/keyboard.h`) synthesizes long-press while the finger is still down and swallows the eventual release. Use it whenever a component distinguishes tap from long-press.
|
||||||
|
- For simpler screens without long-press, `FreeInkApp` / `Screen<N>` (`FreeInkApp.h`) is the ergonomic layer: it owns the interaction buffer, offers a top-to-bottom builder (`takeTop`, footer helpers, etc.), and its `render(input)` call routes the snapshot and dispatches registered action callbacks in one step. `snapshotFrom(input, device)` in `FreeInkUIInputManager.h` builds the `InputSnapshot` with orientation-aware touch mapping.
|
||||||
|
- Everything stays allocation-free and works on the button-only devices unchanged: physical buttons route through the same interaction table via each hit rect's `inputMask`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path 2: Adding touch to existing hand-rolled screens
|
||||||
|
|
||||||
|
For activities that draw their own rows, menus, and buttons, `MappedInputManager` ([src/MappedInputManager.h](../../src/MappedInputManager.h)) exposes bridge helpers. They all return **logical screen coordinates** (orientation already applied via `GfxRenderer::tapToLogical`); never touch raw normalized panel coordinates or the SDK `InputManager` directly.
|
||||||
|
|
||||||
|
| Helper | Use for |
|
||||||
|
|---|---|
|
||||||
|
| `wasScreenTapped(x, y)` | A completed tap (press + release), with logical coords |
|
||||||
|
| `wasScreenTouchDown(x, y)` | Touch-down (held > 90 ms, not yet released): draw selection highlight |
|
||||||
|
| `isScreenTouchHeld(x, y)` | Live contact position while the finger is down (drag tracking) |
|
||||||
|
| `wasTapInRect(x, y, w, h)` | One-off hit test on a rectangle (a single button, a banner) |
|
||||||
|
| `wasListItemTapped(index, count, selected, listTop, listHeight, hasSubtitle)` | Taps on a standard UITheme list; does the row/paging math for you |
|
||||||
|
| `wasListItemTouchedDown(...)` | Same geometry, touch-down phase (highlight before activate) |
|
||||||
|
| `rowTouch(row, top, rowStep, rowCount, xStart, xEnd, rowHeight)` | Any custom band of equal-height rows; returns `RowTouch::None/Down/Tap` |
|
||||||
|
| `colTouch(col, left, colStep, colCount, yStart, yEnd, colWidth)` | Horizontal button bands (dialogs, prompts) |
|
||||||
|
| `wasSwipe()` | Raw swipe direction if you need one beyond the global gestures |
|
||||||
|
| `hasTouch()` | True when the device has a touch panel (rarely needed; helpers simply never fire without one) |
|
||||||
|
|
||||||
|
### Pattern A: standard themed list
|
||||||
|
|
||||||
|
One call per loop; UITheme owns the row geometry ([EpubReaderBookmarksActivity.cpp:123](../../src/activities/reader/EpubReaderBookmarksActivity.cpp)):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int tapped = -1;
|
||||||
|
if (mappedInput.wasListItemTapped(tapped, bookmarks.size(), selectorIndex, listY, listHeight, true)) {
|
||||||
|
selectorIndex = tapped;
|
||||||
|
openBookmark();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern B: custom rows with hold highlight
|
||||||
|
|
||||||
|
For non-theme row layouts, use `rowTouch` and distinguish `Down` (highlight) from `Tap` (activate), as in [EpubReaderFootnotesActivity.cpp](../../src/activities/reader/EpubReaderFootnotesActivity.cpp):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int row = -1;
|
||||||
|
const auto touch = mappedInput.rowTouch(row, listTop, lineHeight, visibleCount,
|
||||||
|
contentX, contentX + contentWidth);
|
||||||
|
if (touch != MappedInputManager::RowTouch::None) {
|
||||||
|
const int touched = scrollOffset + row;
|
||||||
|
if (touch == MappedInputManager::RowTouch::Down) {
|
||||||
|
if (selectedIndex != touched) { selectedIndex = touched; requestUpdate(); }
|
||||||
|
} else { // RowTouch::Tap
|
||||||
|
selectedIndex = touched;
|
||||||
|
activateSelection();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Down` state exists because e-ink repaints are slow: highlight on touch-down gives immediate feedback, activation happens on release.
|
||||||
|
|
||||||
|
### Global gestures: do not reimplement these
|
||||||
|
|
||||||
|
Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling:
|
||||||
|
|
||||||
|
| Gesture | Trigger | Where it is handled |
|
||||||
|
|---|---|---|
|
||||||
|
| Back | Right-swipe starting in the left 25% of the screen | Folded into `Button::Back`, so the existing `wasPressed(Button::Back)` in your activity already fires |
|
||||||
|
| Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) |
|
||||||
|
| Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) |
|
||||||
|
|
||||||
|
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. That is the bar to aim for: bridge helpers should make touch an additive layer over the button flow, not a second input state machine.
|
||||||
|
|
||||||
|
### Bridge rules
|
||||||
|
|
||||||
|
- Handle touch in `loop()` next to the existing button handling, one helper call per interaction zone, and `return` after consuming an event (mirrors the button pattern).
|
||||||
|
- Never call the SDK `InputManager` or read GPIO directly; the HAL rule from the main guide applies to touch too.
|
||||||
|
- Coordinates from the helpers are logical and orientation-correct on all four rotations; test at least Portrait and one Landscape mode before PR.
|
||||||
|
- Nothing to clean up in `onExit()`; the helpers are stateless from the activity's point of view.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building and testing on non-Xteink devices
|
||||||
|
|
||||||
|
Each MCU family is its own binary: X3/X4 are ESP32-C3, Sticky and LilyGo T5 are ESP32-S3, M5Paper v1.1 is a classic ESP32. The Sticky env ships in `platformio.ini` (`pio run -e sticky`). Envs for other devices go in **`platformio.local.ini`**, a gitignored file that PlatformIO merges over `platformio.ini` (see `extra_configs`). Create it next to `platformio.ini`; personal envs, ports, and debug flags live there and never get committed.
|
||||||
|
|
||||||
|
Both envs below extend the repo's `[base]`, so they build against the `freeink-sdk` submodule with all the normal deps and scripts.
|
||||||
|
|
||||||
|
### M5Paper v1.1 (classic ESP32, IT8951 panel)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[env:m5paper_v11]
|
||||||
|
extends = base
|
||||||
|
board = esp32dev
|
||||||
|
board_build.mcu = esp32
|
||||||
|
board_build.flash_mode = qio
|
||||||
|
; CP2104 UART bridge: 921600 drops out on macOS after the stub baud switch
|
||||||
|
upload_speed = 460800
|
||||||
|
build_unflags =
|
||||||
|
${base.build_unflags}
|
||||||
|
; classic ESP32 has UART serial, not USB CDC; Logging.h keys off these
|
||||||
|
-DARDUINO_USB_MODE=1
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DFREEINK_DEVICE_M5PAPER=1
|
||||||
|
; the 63KB 540x960 framebuffer lives in PSRAM (FREEINK_FB_PSRAM auto-on)
|
||||||
|
-DBOARD_HAS_PSRAM
|
||||||
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-m5paper\"
|
||||||
|
-DENABLE_SERIAL_LOG
|
||||||
|
-DLOG_LEVEL=2
|
||||||
|
; touch-first device: hide front-button hint labels
|
||||||
|
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
|
||||||
|
; archive-scan-order workaround: without these a full relink drops Wire's i2c symbols
|
||||||
|
-Wl,-u,i2cInit
|
||||||
|
-Wl,-u,i2cSlaveInit
|
||||||
|
```
|
||||||
|
|
||||||
|
### LilyGo T5 S3 (ESP32-S3, controller-less panel via LovyanGFX)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[env:lilygo_t5s3]
|
||||||
|
extends = base
|
||||||
|
board = esp32-s3-devkitc1-n16r8
|
||||||
|
board_build.mcu = esp32s3
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DFREEINK_DEVICE_LILYGO=1
|
||||||
|
; board injects the parallel-bus pins + PMIC power hooks (BoardT5S3)
|
||||||
|
-DFREEINK_LGFX_EPD_CONFIG=lilygoT5S3LgfxConfig
|
||||||
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-lilygo\"
|
||||||
|
-DENABLE_SERIAL_LOG
|
||||||
|
-DLOG_LEVEL=2
|
||||||
|
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
|
||||||
|
lib_deps =
|
||||||
|
${base.lib_deps}
|
||||||
|
; LgfxEpdConfig for the T5 S3 (pins, PCA9535/TPS65185 power sequence)
|
||||||
|
BoardT5S3=symlink://freeink-sdk/libs/hardware/BoardT5S3
|
||||||
|
; LovyanGFX Panel_EPD drives the controller-less ED047TC1 panel
|
||||||
|
m5stack/M5GFX @ 0.2.20
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `pio run -e m5paper_v11 -t upload` (or `-e lilygo_t5s3`). Gotchas worth knowing:
|
||||||
|
|
||||||
|
- **Flash mode matters.** The M5Paper is `qio`; the X4-family standalone envs need `dio`. A wrong flash-mode header boots into a `partition 0 invalid magic number 0xffff` loop even though esptool verified the write.
|
||||||
|
- **One `FREEINK_DEVICE_*` flag per env** selects the board profile (pins, panel, touch controller) from the SDK's `BoardConfig`. See `freeink-sdk/platformio.sample.ini` for reference envs of every supported device.
|
||||||
|
- **Serial logs:** `[base]` does not enable logging; without `-DENABLE_SERIAL_LOG` a non-default env prints nothing.
|
||||||
|
- No touch hardware on your desk? The X4 build still exercises the same code paths through buttons; touch-specific behavior (tap zones, gestures) needs a real device.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Dictionary
|
||||||
|
|
||||||
|
Look up words while reading an EPUB using an offline StarDict dictionary stored on the SD card.
|
||||||
|
|
||||||
|
## Supported Format
|
||||||
|
|
||||||
|
The reader supports **StarDict** dictionaries. When searching for dictionaries online, look for "StarDict format" or files with `.dict`, `.idx`, and `.ifo` extensions.
|
||||||
|
|
||||||
|
A dictionary folder must contain:
|
||||||
|
|
||||||
|
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
|
||||||
|
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
|
||||||
|
- `.ifo` — metadata (optional)
|
||||||
|
|
||||||
|
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
|
||||||
|
|
||||||
|
## Setting Up a Dictionary
|
||||||
|
|
||||||
|
1. Copy your dictionary folder(s) to `/dictionaries/` on the SD card — one dictionary per folder, e.g. `/dictionaries/webster/webster.idx` + `webster.dict.dz`. A hidden `/.dictionaries/` folder (dot-prefixed) works the same way, for keeping it out of the file browser.
|
||||||
|
2. Open **Settings → Reader → Dictionary** on the device.
|
||||||
|
3. Select a dictionary from the list, or **None** to disable lookups.
|
||||||
|
|
||||||
|
The Dictionary setting only appears when at least one usable dictionary folder exists. Folders containing more than one dictionary (multiple `.idx` stems) are skipped as ambiguous.
|
||||||
|
|
||||||
|
## Looking Up a Word
|
||||||
|
|
||||||
|
Two ways to start a lookup while reading:
|
||||||
|
|
||||||
|
- Open the reader menu (**Confirm**) and choose **Look Up**.
|
||||||
|
- Or set **Settings → Controls → Long-press Menu** to "Dictionary", then hold **Confirm** (~0.4s) on the reading page.
|
||||||
|
|
||||||
|
One word on the page becomes highlighted:
|
||||||
|
|
||||||
|
1. Use **Left/Right** to move between words in reading order, and the side **Up/Down** buttons to jump between lines.
|
||||||
|
2. Press **Confirm** to look up the highlighted word.
|
||||||
|
3. Press **Back** to return to the reader.
|
||||||
|
|
||||||
|
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
|
||||||
|
|
||||||
|
### How Lookup Works
|
||||||
|
|
||||||
|
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
|
||||||
|
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
|
||||||
|
3. **Not found** — a short popup appears and you return to word selection.
|
||||||
|
|
||||||
|
## The Definition Screen
|
||||||
|
|
||||||
|
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
|
||||||
|
|
||||||
|
- **Left/Right** or side **Up/Down** — previous / next page
|
||||||
|
- **Back** — return to word selection
|
||||||
@@ -90,13 +90,19 @@ if (parsedSize != fileSize) {
|
|||||||
|
|
||||||
## `section.bin`
|
## `section.bin`
|
||||||
|
|
||||||
### Version 29
|
### Version 30
|
||||||
|
|
||||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
||||||
also the cache-busting key: if any layout-affecting setting differs from the
|
also the cache-busting key: if any layout-affecting setting differs from the
|
||||||
current reader settings, the section is discarded and rebuilt.
|
current reader settings, the section is discarded and rebuilt.
|
||||||
|
|
||||||
Version 29 includes:
|
Version 30 is binary-identical to version 29. The version was bumped because
|
||||||
|
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
|
||||||
|
measures the shaped visual text), so word positions cached by v29 no longer
|
||||||
|
match what `drawText` renders.
|
||||||
|
|
||||||
|
Version 28 introduced serialized word style bits for underline, strikethrough,
|
||||||
|
superscript, and subscript. The format also includes:
|
||||||
|
|
||||||
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
||||||
image rendering mode, and Focus Reading
|
image rendering mode, and Focus Reading
|
||||||
@@ -119,7 +125,7 @@ import std.mem;
|
|||||||
import std.string;
|
import std.string;
|
||||||
import std.core;
|
import std.core;
|
||||||
|
|
||||||
#define EXPECTED_VERSION 29
|
#define EXPECTED_VERSION 30
|
||||||
#define MAX_STRING_LENGTH 65535
|
#define MAX_STRING_LENGTH 65535
|
||||||
#define FOOTNOTE_NUMBER_LEN 32
|
#define FOOTNOTE_NUMBER_LEN 32
|
||||||
#define FOOTNOTE_HREF_LEN 96
|
#define FOOTNOTE_HREF_LEN 96
|
||||||
|
|||||||
+8
-3
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
|
|||||||
## Join Network Mode
|
## Join Network Mode
|
||||||
|
|
||||||
1. Select **Join Network**.
|
1. Select **Join Network**.
|
||||||
2. Pick a 2.4 GHz Wi-Fi network from the scan results.
|
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
|
||||||
3. Enter the password if prompted.
|
connected network, then other visible saved networks in signal-strength
|
||||||
4. Save credentials if you want the reader to reconnect automatically next time.
|
order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
|
||||||
|
the network list.
|
||||||
|
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
|
||||||
|
results.
|
||||||
|
4. Enter the password if prompted.
|
||||||
|
5. Save credentials if you want the reader to reconnect automatically next time.
|
||||||
|
|
||||||
After connection, the reader shows:
|
After connection, the reader shows:
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule freeink-sdk updated: fdd6105eb3...566fce3d4f
+5
-19
@@ -44,16 +44,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
|
const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
|
||||||
|
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
|
||||||
|
|
||||||
if (!isCombining && prevCp != 0) {
|
if (!isCombining && prevCp != 0) {
|
||||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int glyphBaseX =
|
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||||
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
|
glyph->left, glyph->width)
|
||||||
: lastBaseX;
|
: lastBaseX;
|
||||||
const int glyphBaseY = startY - raiseBy;
|
const int glyphBaseY = startY - raiseBy;
|
||||||
|
|
||||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||||
@@ -154,21 +155,6 @@ uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
|
|||||||
return cp;
|
return cp;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EpdFont::hasGlyph(const uint32_t cp) const {
|
|
||||||
const int count = data->intervalCount;
|
|
||||||
if (count > 0) {
|
|
||||||
const EpdUnicodeInterval* intervals = data->intervals;
|
|
||||||
const auto it =
|
|
||||||
std::upper_bound(intervals, intervals + count, cp,
|
|
||||||
[](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
|
||||||
if (it != intervals && cp <= (it - 1)->last) return true;
|
|
||||||
}
|
|
||||||
if (data->glyphMissHandler) {
|
|
||||||
return data->glyphMissHandler(data->glyphMissCtx, cp) != nullptr;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||||
const int count = data->intervalCount;
|
const int count = data->intervalCount;
|
||||||
if (count == 0 && !data->glyphMissHandler) return nullptr;
|
if (count == 0 && !data->glyphMissHandler) return nullptr;
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ class EpdFont {
|
|||||||
|
|
||||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||||
|
|
||||||
/// True when the font actually provides `cp` (interval table or on-demand
|
|
||||||
/// SD load) — unlike getGlyph(), never satisfied by the replacement glyph.
|
|
||||||
/// Used by layout shaping to decide whether a substitution (ligature,
|
|
||||||
/// Arabic presentation form) can really be drawn.
|
|
||||||
bool hasGlyph(uint32_t cp) const;
|
|
||||||
|
|
||||||
/// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints.
|
/// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints.
|
||||||
/// Returns 0 if no kerning data exists for the pair.
|
/// Returns 0 if no kerning data exists for the pair.
|
||||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
|
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
|
||||||
|
|||||||
+76
-11
@@ -36,24 +36,71 @@ namespace combiningMark {
|
|||||||
|
|
||||||
constexpr int MIN_GAP_PX = 1;
|
constexpr int MIN_GAP_PX = 1;
|
||||||
|
|
||||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
/// Placement of a mark relative to its base glyph. The default heuristic —
|
||||||
/// is visually centered over the base glyph's bitmap.
|
/// centered over the base, raised clear of its top — suits Latin diacritics
|
||||||
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
|
||||||
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
|
/// on position: dagesh sits inside the letter body, the shin/sin dots
|
||||||
|
/// distinguish the letter by sitting over its right/left arm, and holam hangs
|
||||||
|
/// over the left corner. "Native" anchors keep the glyph's font-designed
|
||||||
|
/// height (which may overlap the base) instead of raising it.
|
||||||
|
enum class Anchor : uint8_t {
|
||||||
|
CenterRaised, ///< centered over the base, lifted above its top (default)
|
||||||
|
CenterNative, ///< centered over the base at font-native height
|
||||||
|
RightNative, ///< right edges aligned, font-native height
|
||||||
|
LeftNative, ///< left edges aligned, font-native height
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Anchor anchorFor(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
|
||||||
|
case 0x05BA: // holam haser for vav: straight above the vav stem
|
||||||
|
return Anchor::CenterNative;
|
||||||
|
case 0x05C1: // shin dot: over the letter's right arm
|
||||||
|
return Anchor::RightNative;
|
||||||
|
case 0x05B9: // holam: above the letter's left corner
|
||||||
|
case 0x05C2: // sin dot: over the letter's left arm
|
||||||
|
return Anchor::LeftNative;
|
||||||
|
default:
|
||||||
|
return Anchor::CenterRaised;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rotated-90CW variant of centerOver. In the rotated coordinate system
|
/// Horizontal offset from the base bitmap's left edge to the mark bitmap's
|
||||||
|
/// left edge for a given anchor.
|
||||||
|
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
|
||||||
|
switch (anchor) {
|
||||||
|
case Anchor::LeftNative:
|
||||||
|
return 0;
|
||||||
|
case Anchor::RightNative:
|
||||||
|
return baseWidth - markWidth;
|
||||||
|
default:
|
||||||
|
return baseWidth / 2 - markWidth / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||||
|
/// lands at its anchor position over the base glyph's bitmap.
|
||||||
|
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
|
||||||
|
const int markLeft, const int markWidth) {
|
||||||
|
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
|
||||||
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
||||||
/// every left/width term inverts sign.
|
/// every left/width term inverts sign.
|
||||||
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
|
||||||
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft;
|
const int baseWidth, const int markLeft, const int markWidth) {
|
||||||
|
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For combining marks that sit entirely above the baseline, compute how many
|
/// For combining marks that sit entirely above the baseline, compute how many
|
||||||
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
||||||
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
||||||
/// below the baseline (e.g. cedilla, dot-below, ogonek).
|
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
|
||||||
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
/// keep the font-native height (dagesh must stay inside the letter, the
|
||||||
|
/// shin/sin dots touch its arms).
|
||||||
|
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
|
||||||
|
if (anchor != Anchor::CenterRaised) return 0;
|
||||||
if (markTop - markHeight <= 0) return 0;
|
if (markTop - markHeight <= 0) return 0;
|
||||||
const int gap = markTop - markHeight - baseTop;
|
const int gap = markTop - markHeight - baseTop;
|
||||||
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
||||||
@@ -61,6 +108,20 @@ constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
|||||||
|
|
||||||
} // namespace combiningMark
|
} // namespace combiningMark
|
||||||
|
|
||||||
|
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
|
||||||
|
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
|
||||||
|
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
|
||||||
|
/// identical across both toolchains.
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
|
||||||
|
#define EPD_PACKED_END __pragma(pack(pop))
|
||||||
|
#define EPD_PACKED_ATTR
|
||||||
|
#else
|
||||||
|
#define EPD_PACKED_BEGIN
|
||||||
|
#define EPD_PACKED_END
|
||||||
|
#define EPD_PACKED_ATTR __attribute__((packed))
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||||
@@ -95,17 +156,21 @@ typedef struct {
|
|||||||
|
|
||||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint16_t codepoint; ///< Unicode codepoint
|
uint16_t codepoint; ///< Unicode codepoint
|
||||||
uint8_t classId; ///< 1-based kerning class ID
|
uint8_t classId; ///< 1-based kerning class ID
|
||||||
} __attribute__((packed)) EpdKernClassEntry;
|
} EPD_PACKED_ATTR EpdKernClassEntry;
|
||||||
|
EPD_PACKED_END
|
||||||
|
|
||||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||||
} __attribute__((packed)) EpdLigaturePair;
|
} EPD_PACKED_ATTR EpdLigaturePair;
|
||||||
|
EPD_PACKED_END
|
||||||
|
|
||||||
/// Data stored for FONT AS A WHOLE
|
/// Data stored for FONT AS A WHOLE
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -28,16 +28,10 @@ const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) co
|
|||||||
return getFont(style)->getGlyph(cp);
|
return getFont(style)->getGlyph(cp);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EpdFontFamily::hasGlyph(const uint32_t cp, const Style style) const { return getFont(style)->hasGlyph(cp); }
|
|
||||||
|
|
||||||
int8_t EpdFontFamily::getKerning(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
|
int8_t EpdFontFamily::getKerning(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
|
||||||
return getFont(style)->getKerning(leftCp, rightCp);
|
return getFont(style)->getKerning(leftCp, rightCp);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t EpdFontFamily::getLigature(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
|
|
||||||
return getFont(style)->getLigature(leftCp, rightCp);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t EpdFontFamily::applyLigatures(const uint32_t cp, const char*& text, const Style style) const {
|
uint32_t EpdFontFamily::applyLigatures(const uint32_t cp, const char*& text, const Style style) const {
|
||||||
return getFont(style)->applyLigatures(cp, text);
|
return getFont(style)->applyLigatures(cp, text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ class EpdFontFamily {
|
|||||||
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
|
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
|
||||||
const EpdFontData* getData(Style style = REGULAR) const;
|
const EpdFontData* getData(Style style = REGULAR) const;
|
||||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||||
bool hasGlyph(uint32_t cp, Style style = REGULAR) const;
|
|
||||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
||||||
uint32_t getLigature(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
|
||||||
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
||||||
static constexpr bool hasTextDecoration(const Style style) {
|
static constexpr bool hasTextDecoration(const Style style) {
|
||||||
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
|
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
|
||||||
|
|||||||
+54
-44
@@ -68,6 +68,22 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c
|
|||||||
const char* asCStr(const std::string& s) { return s.c_str(); }
|
const char* asCStr(const std::string& s) { return s.c_str(); }
|
||||||
const char* asCStr(const char* s) { return s; }
|
const char* asCStr(const char* s) { return s; }
|
||||||
|
|
||||||
|
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
|
||||||
|
// current capacity. Freeing + reallocating slightly different sizes every page
|
||||||
|
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
|
||||||
|
// next page's need), eroding the largest contiguous block all session. With
|
||||||
|
// reuse, capacities converge on the book's max page after a few turns and page
|
||||||
|
// turns stop touching the allocator. Only three small instantiations exist
|
||||||
|
// (interval/glyph/byte arrays), so template bloat is negligible.
|
||||||
|
template <typename T, typename CapT>
|
||||||
|
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
|
||||||
|
if (buf && capacity >= needed) return true;
|
||||||
|
delete[] buf;
|
||||||
|
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
|
||||||
|
capacity = buf ? static_cast<CapT>(needed) : 0;
|
||||||
|
return buf != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SdCardFont::~SdCardFont() { freeAll(); }
|
SdCardFont::~SdCardFont() { freeAll(); }
|
||||||
@@ -83,6 +99,9 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
|||||||
s.miniBitmap = nullptr;
|
s.miniBitmap = nullptr;
|
||||||
s.miniIntervalCount = 0;
|
s.miniIntervalCount = 0;
|
||||||
s.miniGlyphCount = 0;
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniIntervalCapacity = 0;
|
||||||
|
s.miniGlyphCapacity = 0;
|
||||||
|
s.miniBitmapCapacity = 0;
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
memset(&s.miniData, 0, sizeof(s.miniData));
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
s.epdFont.data = &s.stubData;
|
s.epdFont.data = &s.stubData;
|
||||||
@@ -109,6 +128,9 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) {
|
|||||||
s.miniKernRightEntryCount = 0;
|
s.miniKernRightEntryCount = 0;
|
||||||
s.miniKernLeftClassCount = 0;
|
s.miniKernLeftClassCount = 0;
|
||||||
s.miniKernRightClassCount = 0;
|
s.miniKernRightClassCount = 0;
|
||||||
|
s.miniKernLeftCapacity = 0;
|
||||||
|
s.miniKernRightCapacity = 0;
|
||||||
|
s.miniKernMatrixCapacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdCardFont::freeStyleAll(PerStyle& s) {
|
void SdCardFont::freeStyleAll(PerStyle& s) {
|
||||||
@@ -311,13 +333,13 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui
|
|||||||
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
|
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: allocate the three mini buffers. The matrix is <1KB in practice
|
// Step 4: size the three mini buffers (reused across pages when they fit; the
|
||||||
// (<30 × <30 × 1 byte) so fragmentation is a non-issue.
|
// per-page sizes vary by a few entries, which as free+realloc churn was punching
|
||||||
|
// non-coalescing holes in the heap every page turn).
|
||||||
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
|
||||||
s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount];
|
if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) ||
|
||||||
s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount];
|
!ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) ||
|
||||||
s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes];
|
!ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) {
|
||||||
if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
|
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
|
||||||
matrixBytes);
|
matrixBytes);
|
||||||
freeStyleMiniKern(s);
|
freeStyleMiniKern(s);
|
||||||
@@ -793,12 +815,19 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
return missed;
|
return missed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build mini intervals from sorted codepoints
|
// Build mini intervals from sorted codepoints. Reset counts and fall back to the
|
||||||
freeStyleMiniData(s);
|
// stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits
|
||||||
|
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
|
||||||
|
s.miniIntervalCount = 0;
|
||||||
|
s.miniGlyphCount = 0;
|
||||||
|
s.miniKernLeftEntryCount = 0;
|
||||||
|
s.miniKernRightEntryCount = 0;
|
||||||
|
s.miniKernLeftClassCount = 0;
|
||||||
|
s.miniKernRightClassCount = 0;
|
||||||
|
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||||
|
s.epdFont.data = &s.stubData;
|
||||||
|
|
||||||
uint32_t intervalCapacity = validCount;
|
if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) {
|
||||||
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
|
|
||||||
if (!s.miniIntervals) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
@@ -816,15 +845,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate mini glyph array
|
// Mini glyph array (reused across pages when it fits)
|
||||||
s.miniGlyphCount = validCount;
|
if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) {
|
||||||
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
|
|
||||||
if (!s.miniGlyphs) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
freeStyleMiniData(s);
|
freeStyleMiniData(s);
|
||||||
return static_cast<int>(cpCount);
|
return static_cast<int>(cpCount);
|
||||||
}
|
}
|
||||||
|
s.miniGlyphCount = validCount;
|
||||||
|
|
||||||
// Build sorted read order for sequential I/O
|
// Build sorted read order for sequential I/O
|
||||||
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
|
||||||
@@ -891,8 +919,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
|||||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1];
|
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||||||
if (!s.miniBitmap) {
|
|
||||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||||
delete[] readOrder;
|
delete[] readOrder;
|
||||||
delete[] mappings;
|
delete[] mappings;
|
||||||
@@ -1061,28 +1088,6 @@ void SdCardFont::mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sor
|
|||||||
advanceTableSize_[styleIdx] = k;
|
advanceTableSize_[styleIdx] = k;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SdCardFont::hasGlyphMeta(uint8_t styleIdx, uint32_t codepoint) const {
|
|
||||||
styleIdx &= (MAX_STYLES - 1);
|
|
||||||
const PerStyle& s = styles_[styleIdx];
|
|
||||||
if (!loaded_ || !s.present) return false;
|
|
||||||
return findGlobalGlyphIndex(s, codepoint) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t SdCardFont::ensureAdvance(uint8_t styleIdx, uint32_t codepoint) {
|
|
||||||
styleIdx &= (MAX_STYLES - 1);
|
|
||||||
if (!loaded_ || !styles_[styleIdx].present) return 0;
|
|
||||||
const uint16_t cached = getAdvance(codepoint, styleIdx);
|
|
||||||
if (cached != 0) return cached;
|
|
||||||
// 0 is either "not in table" or a genuine zero-width glyph; only pay the
|
|
||||||
// SD read when the resident interval table says the glyph exists.
|
|
||||||
if (findGlobalGlyphIndex(styles_[styleIdx], codepoint) < 0) return 0;
|
|
||||||
uint32_t cp = codepoint;
|
|
||||||
fetchAdvancesForCodepoints(&cp, 1, static_cast<uint8_t>(1u << styleIdx));
|
|
||||||
return getAdvance(codepoint, styleIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t SdCardFont::styleIdxFromMissCtx(void* ctx) { return static_cast<OverflowContext*>(ctx)->styleIdx; }
|
|
||||||
|
|
||||||
bool SdCardFont::hasAdvanceTable() const {
|
bool SdCardFont::hasAdvanceTable() const {
|
||||||
for (uint8_t i = 0; i < MAX_STYLES; i++) {
|
for (uint8_t i = 0; i < MAX_STYLES; i++) {
|
||||||
if (advanceTable_[i]) return true;
|
if (advanceTable_[i]) return true;
|
||||||
@@ -1217,7 +1222,8 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename Iter>
|
template <typename Iter>
|
||||||
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||||
|
const char* extraText) {
|
||||||
if (!loaded_) return -1;
|
if (!loaded_) return -1;
|
||||||
styleMask = resolveStyleMask(styleMask);
|
styleMask = resolveStyleMask(styleMask);
|
||||||
if (styleMask == 0) return 0;
|
if (styleMask == 0) return 0;
|
||||||
@@ -1237,6 +1243,9 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
|||||||
for (auto it = begin; it != end && !hitCap; ++it) {
|
for (auto it = begin; it != end && !hitCap; ++it) {
|
||||||
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
}
|
}
|
||||||
|
if (extraText && !hitCap) {
|
||||||
|
hitCap = collectUniqueCodepoints(extraText, codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||||
|
}
|
||||||
|
|
||||||
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
||||||
codepoints[cpCount++] = ' ';
|
codepoints[cpCount++] = ' ';
|
||||||
@@ -1254,12 +1263,13 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
|||||||
return totalMissed;
|
return totalMissed;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) {
|
||||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
|
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) {
|
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
|
||||||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask);
|
const char* extraText) {
|
||||||
|
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Stats ---
|
// --- Stats ---
|
||||||
|
|||||||
+24
-22
@@ -47,31 +47,17 @@ class SdCardFont {
|
|||||||
// Build a compact advance-only table for layout measurement.
|
// Build a compact advance-only table for layout measurement.
|
||||||
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||||
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
||||||
|
// extraText: optional additional codepoints to warm in the same SD pass
|
||||||
|
// (e.g. shaped Arabic presentation forms the measurement path will look up).
|
||||||
// Returns number of codepoints not found in font coverage.
|
// Returns number of codepoints not found in font coverage.
|
||||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
|
||||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
|
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
|
||||||
|
const char* extraText = nullptr);
|
||||||
|
|
||||||
// Look up advanceX for a codepoint from the advance table.
|
// Look up advanceX for a codepoint from the advance table.
|
||||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||||
uint16_t getAdvance(uint32_t codepoint, uint8_t style) const;
|
uint16_t getAdvance(uint32_t codepoint, uint8_t style) const;
|
||||||
|
|
||||||
// Layout-side metric access — the per-glyph path CpFontAdapter uses while
|
|
||||||
// the FreeInkBook engine measures a chapter. Neither touches bitmap data:
|
|
||||||
// going through getGlyph() would stream every occurrence's bitmap through
|
|
||||||
// the 8-slot overflow ring (one SD read per character — a crawling build).
|
|
||||||
//
|
|
||||||
// hasGlyphMeta answers existence from the resident interval table (no I/O).
|
|
||||||
bool hasGlyphMeta(uint8_t styleIdx, uint32_t codepoint) const;
|
|
||||||
// ensureAdvance returns the 12.4 fixed-point advanceX, fetching the glyph
|
|
||||||
// METADATA from SD once on a miss and merging it into the persistent
|
|
||||||
// advance table (so each unique codepoint costs one small read per
|
|
||||||
// session). Returns 0 when the style lacks the codepoint.
|
|
||||||
uint16_t ensureAdvance(uint8_t styleIdx, uint32_t codepoint);
|
|
||||||
|
|
||||||
// Recover the style index from an EpdFontData::glyphMissCtx (see
|
|
||||||
// fromMissCtx below for recovering the SdCardFont itself).
|
|
||||||
static uint8_t styleIdxFromMissCtx(void* ctx);
|
|
||||||
|
|
||||||
// Returns true if advance table is populated for at least one style.
|
// Returns true if advance table is populated for at least one style.
|
||||||
bool hasAdvanceTable() const;
|
bool hasAdvanceTable() const;
|
||||||
|
|
||||||
@@ -157,11 +143,13 @@ class SdCardFont {
|
|||||||
|
|
||||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||||
|
EPD_PACKED_BEGIN
|
||||||
struct BmpInterval16 {
|
struct BmpInterval16 {
|
||||||
uint16_t first;
|
uint16_t first;
|
||||||
uint16_t last;
|
uint16_t last;
|
||||||
uint16_t offset;
|
uint16_t offset;
|
||||||
} __attribute__((packed));
|
} EPD_PACKED_ATTR;
|
||||||
|
EPD_PACKED_END
|
||||||
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||||
BmpInterval16* bmpIntervals = nullptr;
|
BmpInterval16* bmpIntervals = nullptr;
|
||||||
bool intervalsAreBmp16 = false;
|
bool intervalsAreBmp16 = false;
|
||||||
@@ -180,13 +168,22 @@ class SdCardFont {
|
|||||||
// Stub EpdFontData returned when not prewarmed
|
// Stub EpdFontData returned when not prewarmed
|
||||||
EpdFontData stubData{};
|
EpdFontData stubData{};
|
||||||
|
|
||||||
// Mini EpdFontData built during prewarm
|
// Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages
|
||||||
|
// (capacities below track allocated sizes): freeing and reallocating slightly
|
||||||
|
// different sizes on every page turn was a primary heap fragmenter — each page's
|
||||||
|
// freed hole rarely fit the next page's need, so maxAlloc eroded all session.
|
||||||
|
// After a few pages the capacities converge on the book's max and page turns
|
||||||
|
// stop allocating entirely. freeStyleMiniData() still releases everything (and
|
||||||
|
// zeroes capacities) for style eviction / font unload.
|
||||||
EpdFontData miniData{};
|
EpdFontData miniData{};
|
||||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||||
EpdGlyph* miniGlyphs = nullptr;
|
EpdGlyph* miniGlyphs = nullptr;
|
||||||
uint8_t* miniBitmap = nullptr;
|
uint8_t* miniBitmap = nullptr;
|
||||||
uint32_t miniIntervalCount = 0;
|
uint32_t miniIntervalCount = 0;
|
||||||
uint32_t miniGlyphCount = 0;
|
uint32_t miniGlyphCount = 0;
|
||||||
|
uint32_t miniIntervalCapacity = 0;
|
||||||
|
uint32_t miniGlyphCapacity = 0;
|
||||||
|
uint32_t miniBitmapCapacity = 0;
|
||||||
|
|
||||||
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
||||||
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
||||||
@@ -201,6 +198,10 @@ class SdCardFont {
|
|||||||
uint8_t miniKernLeftClassCount = 0;
|
uint8_t miniKernLeftClassCount = 0;
|
||||||
uint8_t miniKernRightClassCount = 0;
|
uint8_t miniKernRightClassCount = 0;
|
||||||
int8_t* miniKernMatrix = nullptr;
|
int8_t* miniKernMatrix = nullptr;
|
||||||
|
// Kept-if-fits capacities, same rationale as the mini glyph buffers above.
|
||||||
|
uint16_t miniKernLeftCapacity = 0;
|
||||||
|
uint16_t miniKernRightCapacity = 0;
|
||||||
|
uint32_t miniKernMatrixCapacity = 0;
|
||||||
|
|
||||||
// The EpdFont whose data pointer we manage
|
// The EpdFont whose data pointer we manage
|
||||||
EpdFont epdFont{&stubData};
|
EpdFont epdFont{&stubData};
|
||||||
@@ -266,7 +267,8 @@ class SdCardFont {
|
|||||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||||
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||||
template <typename Iter>
|
template <typename Iter>
|
||||||
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask);
|
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||||
|
const char* extraText = nullptr);
|
||||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||||
|
|
||||||
// Global helpers
|
// Global helpers
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
|||||||
!NotoSerif/**
|
!NotoSerif/**
|
||||||
!NotoSans/
|
!NotoSans/
|
||||||
!NotoSans/**
|
!NotoSans/**
|
||||||
|
!NotoSansArabic/
|
||||||
|
!NotoSansArabic/**
|
||||||
!NotoSansHebrew/
|
!NotoSansHebrew/
|
||||||
!NotoSansHebrew/**
|
!NotoSansHebrew/**
|
||||||
!OpenDyslexic/
|
!OpenDyslexic/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
+1872
-1256
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2244
-1483
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -31,19 +31,48 @@ done
|
|||||||
UI_FONT_SIZES=(10 12)
|
UI_FONT_SIZES=(10 12)
|
||||||
UI_FONT_STYLES=("Regular" "Bold")
|
UI_FONT_STYLES=("Regular" "Bold")
|
||||||
|
|
||||||
|
# Arabic glyphs for UI text (menus, file browser titles). The built-in fonts
|
||||||
|
# must cover the *output* of MiniBidi's do_shape() — contextual presentation
|
||||||
|
# forms — not base letters, or shaped UI text silently drops glyphs.
|
||||||
|
# Curated for firmware-size budget: core Arabic (Presentation Forms-B,
|
||||||
|
# incl. the Lam-Alef ligature forms) plus the Farsi/Urdu extra letters'
|
||||||
|
# Presentation Forms-A blocks, the few characters shaping leaves at their
|
||||||
|
# base codepoint, Arabic punctuation, and both digit sets. No harakat and
|
||||||
|
# no Sindhi/Pashto/Kurdish forms — book text gets those from SD-card fonts.
|
||||||
|
ARABIC_INTERVALS=(
|
||||||
|
--additional-intervals 0x060C,0x060C # Arabic comma
|
||||||
|
--additional-intervals 0x061B,0x061B # Arabic semicolon
|
||||||
|
--additional-intervals 0x061F,0x061F # Arabic question mark
|
||||||
|
--additional-intervals 0x0621,0x0621 # hamza (non-joining, never shaped)
|
||||||
|
--additional-intervals 0x0640,0x0640 # tatweel
|
||||||
|
--additional-intervals 0x0660,0x0669 # Arabic-Indic digits
|
||||||
|
--additional-intervals 0x06BA,0x06BA # noon ghunna base (initial/medial keep base cp)
|
||||||
|
--additional-intervals 0x06D4,0x06D4 # Urdu full stop
|
||||||
|
--additional-intervals 0x06F0,0x06F9 # extended Arabic-Indic digits (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFB56,0xFB59 # peh (Farsi)
|
||||||
|
--additional-intervals 0xFB66,0xFB69 # tteh (Urdu)
|
||||||
|
--additional-intervals 0xFB7A,0xFB7D # tcheh (Farsi)
|
||||||
|
--additional-intervals 0xFB88,0xFB95 # ddal, jeh, rreh (Urdu), keheh, gaf (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFB9E,0xFB9F # noon ghunna isolated/final (Urdu)
|
||||||
|
--additional-intervals 0xFBA6,0xFBB1 # heh goal, heh doachashmee, yeh barree(+hamza) (Urdu)
|
||||||
|
--additional-intervals 0xFBFC,0xFBFF # farsi yeh (Farsi/Urdu)
|
||||||
|
--additional-intervals 0xFE80,0xFEFC # Presentation Forms-B: core Arabic + Lam-Alef
|
||||||
|
)
|
||||||
|
|
||||||
for size in ${UI_FONT_SIZES[@]}; do
|
for size in ${UI_FONT_SIZES[@]}; do
|
||||||
for style in ${UI_FONT_STYLES[@]}; do
|
for style in ${UI_FONT_STYLES[@]}; do
|
||||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
||||||
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
||||||
|
arabic_path="../builtinFonts/source/NotoSansArabic/NotoSansArabic-${style}.ttf"
|
||||||
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
||||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
||||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
# are filled from it while every glyph Ubuntu already has stays unchanged
|
||||||
# (fontstack is ordered by descending priority).
|
# (fontstack is ordered by descending priority).
|
||||||
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||||
output_path="../builtinFonts/${font_name}.h"
|
output_path="../builtinFonts/${font_name}.h"
|
||||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path $viet_path \
|
||||||
--additional-intervals 0x05D0,0x05EA > $output_path
|
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path
|
||||||
echo "Generated $output_path"
|
echo "Generated $output_path"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -51,7 +80,8 @@ done
|
|||||||
python fontconvert.py notosans_8_regular 8 \
|
python fontconvert.py notosans_8_regular 8 \
|
||||||
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
||||||
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
||||||
--additional-intervals 0x05D0,0x05EA > ../builtinFonts/notosans_8_regular.h
|
../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf \
|
||||||
|
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > ../builtinFonts/notosans_8_regular.h
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Running compression verification..."
|
echo "Running compression verification..."
|
||||||
|
|||||||
@@ -0,0 +1,939 @@
|
|||||||
|
#include "Epub.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <JpegToBmpConverter.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <PngToBmpConverter.h>
|
||||||
|
#include <Utf8.h>
|
||||||
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
|
#include "Epub/parsers/ContentOpfParser.h"
|
||||||
|
#include "Epub/parsers/TocNavParser.h"
|
||||||
|
#include "Epub/parsers/TocNcxParser.h"
|
||||||
|
|
||||||
|
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||||
|
const auto containerPath = "META-INF/container.xml";
|
||||||
|
size_t containerSize;
|
||||||
|
|
||||||
|
// Get file size without loading it all into heap
|
||||||
|
if (!getItemSize(containerPath, &containerSize)) {
|
||||||
|
LOG_ERR("EBP", "Could not find or size META-INF/container.xml");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerParser containerParser(containerSize);
|
||||||
|
|
||||||
|
if (!containerParser.setup()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream read (reusing your existing stream logic)
|
||||||
|
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||||
|
LOG_ERR("EBP", "Could not read META-INF/container.xml");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the result
|
||||||
|
if (containerParser.fullPath.empty()) {
|
||||||
|
LOG_ERR("EBP", "Could not find valid rootfile in container.xml");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*contentOpfFile = std::move(containerParser.fullPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const bool writeSpineEntries) {
|
||||||
|
std::string contentOpfFilePath;
|
||||||
|
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||||
|
LOG_ERR("EBP", "Could not find content.opf in zip");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str());
|
||||||
|
|
||||||
|
size_t contentOpfSize;
|
||||||
|
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||||
|
LOG_ERR("EBP", "Could not get size of content.opf");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize,
|
||||||
|
writeSpineEntries ? bookMetadataCache.get() : nullptr);
|
||||||
|
if (!opfParser.setup()) {
|
||||||
|
LOG_ERR("EBP", "Could not setup content.opf parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read content.opf");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab data from opfParser into epub. Normalize titles to NFC so NFD (combining
|
||||||
|
// mark) text renders correctly — the device fonts have no mark positioning.
|
||||||
|
bookMetadata.title = utf8ComposeNfc(opfParser.title);
|
||||||
|
bookMetadata.author = opfParser.author;
|
||||||
|
bookMetadata.language = opfParser.language;
|
||||||
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
|
|
||||||
|
// Guide-based cover fallback: if no cover found via metadata/properties,
|
||||||
|
// try extracting the image reference from the guide's cover page XHTML
|
||||||
|
if (bookMetadata.coverItemHref.empty() && !opfParser.guideCoverPageHref.empty()) {
|
||||||
|
LOG_DBG("EBP", "No cover from metadata, trying guide cover page: %s", opfParser.guideCoverPageHref.c_str());
|
||||||
|
size_t coverPageSize;
|
||||||
|
uint8_t* coverPageData = readItemContentsToBytes(opfParser.guideCoverPageHref, &coverPageSize, true);
|
||||||
|
if (coverPageData) {
|
||||||
|
const std::string coverPageHtml(reinterpret_cast<char*>(coverPageData), coverPageSize);
|
||||||
|
free(coverPageData);
|
||||||
|
|
||||||
|
// Determine base path of the cover page for resolving relative image references
|
||||||
|
std::string coverPageBase;
|
||||||
|
const auto lastSlash = opfParser.guideCoverPageHref.rfind('/');
|
||||||
|
if (lastSlash != std::string::npos) {
|
||||||
|
coverPageBase = opfParser.guideCoverPageHref.substr(0, lastSlash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for image references: xlink:href="..." (SVG) and src="..." (img)
|
||||||
|
std::string imageRef;
|
||||||
|
for (const char* pattern : {"xlink:href=\"", "src=\""}) {
|
||||||
|
auto pos = coverPageHtml.find(pattern);
|
||||||
|
while (pos != std::string::npos) {
|
||||||
|
pos += strlen(pattern);
|
||||||
|
const auto endPos = coverPageHtml.find('"', pos);
|
||||||
|
if (endPos != std::string::npos) {
|
||||||
|
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
|
||||||
|
// Cover BMP generation supports JPG/PNG only; skip GIF so an unsupported wrapper image
|
||||||
|
// does not block a later supported cover reference.
|
||||||
|
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref)) {
|
||||||
|
imageRef = ref;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = coverPageHtml.find(pattern, pos);
|
||||||
|
}
|
||||||
|
if (!imageRef.empty()) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageRef.empty()) {
|
||||||
|
bookMetadata.coverItemHref = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(coverPageBase + imageRef));
|
||||||
|
LOG_DBG("EBP", "Found cover image from guide: %s", bookMetadata.coverItemHref.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||||
|
|
||||||
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
|
tocNcxItem = opfParser.tocNcxPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opfParser.tocNavPath.empty()) {
|
||||||
|
tocNavItem = opfParser.tocNavPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opfParser.cssFiles.empty()) {
|
||||||
|
cssFiles = opfParser.cssFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Successfully parsed content.opf");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::parseTocNcxFile() const {
|
||||||
|
// the ncx file should have been specified in the content.opf file
|
||||||
|
if (tocNcxItem.empty()) {
|
||||||
|
LOG_DBG("EBP", "No ncx file specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||||
|
|
||||||
|
size_t ncxSize;
|
||||||
|
if (!getItemSize(tocNcxItem, &ncxSize)) {
|
||||||
|
LOG_ERR("EBP", "Could not get size of toc ncx file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
|
if (!ncxParser.setup()) {
|
||||||
|
LOG_ERR("EBP", "Could not setup toc ncx parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream the decompressed NCX straight into the parser instead of round-tripping
|
||||||
|
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||||
|
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read toc ncx file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Parsed TOC items");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::parseTocNavFile() const {
|
||||||
|
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||||
|
if (tocNavItem.empty()) {
|
||||||
|
LOG_DBG("EBP", "No nav file specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||||
|
|
||||||
|
size_t navSize;
|
||||||
|
if (!getItemSize(tocNavItem, &navSize)) {
|
||||||
|
LOG_ERR("EBP", "Could not get size of toc nav file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||||
|
// and the HTMLX nav file will have hrefs relative to itself
|
||||||
|
const std::string navContentBasePath = tocNavItem.substr(0, tocNavItem.find_last_of('/') + 1);
|
||||||
|
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
||||||
|
|
||||||
|
if (!navParser.setup()) {
|
||||||
|
LOG_ERR("EBP", "Could not setup toc nav parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream the decompressed nav document straight into the parser instead of round-tripping
|
||||||
|
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||||
|
if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read toc nav file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Epub::discoverCssFilesFromZip() {
|
||||||
|
const std::string& opfDir = contentBasePath;
|
||||||
|
ZipFile zf(filepath);
|
||||||
|
|
||||||
|
if (!zf.enumerateFilePaths([&](std::string_view filePath) {
|
||||||
|
if (!opfDir.empty() && filePath.find(opfDir) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FsHelpers::hasCssExtension(filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::find(cssFiles.begin(), cssFiles.end(), filePath) != cssFiles.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Discovered CSS file via ZIP enumeration: %.*s", (int)filePath.size(), filePath.data());
|
||||||
|
cssFiles.push_back(std::string{filePath});
|
||||||
|
})) {
|
||||||
|
LOG_ERR("EBP", "Failed to enumerate ZIP file paths for CSS discovery");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Epub::parseCssFiles() const {
|
||||||
|
// Maximum CSS file size we'll attempt to parse (uncompressed)
|
||||||
|
// Larger files risk memory exhaustion on ESP32
|
||||||
|
constexpr size_t MAX_CSS_FILE_SIZE = 128 * 1024; // 128KB
|
||||||
|
// Minimum heap required before attempting CSS parsing
|
||||||
|
constexpr size_t MIN_HEAP_FOR_CSS_PARSING = 64 * 1024; // 64KB
|
||||||
|
|
||||||
|
if (cssFiles.empty()) {
|
||||||
|
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "CSS files to parse: %zu", cssFiles.size());
|
||||||
|
|
||||||
|
// See if we have a cached version of the CSS rules
|
||||||
|
if (cssParser->hasCache()) {
|
||||||
|
LOG_DBG("EBP", "CSS cache exists, skipping parseCssFiles");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some converters emit one byte-identical stylesheet per chapter (100+ .css
|
||||||
|
// entries), and each parse costs a zip locate plus an SD extract round-trip.
|
||||||
|
// Map every CSS path to its central-directory (CRC32, compressed size) in a
|
||||||
|
// single scan and parse only the first of each identical pair. Rules merge
|
||||||
|
// into one global set, so dropping exact duplicates cannot lose styles. A
|
||||||
|
// path that never matches a directory entry keeps key 0 and always parses.
|
||||||
|
std::vector<uint64_t> dedupKeys(cssFiles.size(), 0);
|
||||||
|
if (cssFiles.size() > 1) {
|
||||||
|
std::unordered_map<std::string, size_t> pathToIndex;
|
||||||
|
pathToIndex.reserve(cssFiles.size());
|
||||||
|
for (size_t i = 0; i < cssFiles.size(); i++) {
|
||||||
|
pathToIndex.emplace(FsHelpers::normalisePath(cssFiles[i]), i);
|
||||||
|
}
|
||||||
|
ZipFile(filepath).enumerateFileEntries([&](std::string_view entryPath, uint32_t crc32, uint32_t compressedSize) {
|
||||||
|
if (!FsHelpers::hasCssExtension(entryPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto it = pathToIndex.find(std::string{entryPath});
|
||||||
|
if (it != pathToIndex.end()) {
|
||||||
|
dedupKeys[it->second] = (static_cast<uint64_t>(crc32) << 32) | compressedSize;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
std::vector<uint64_t> seenKeys;
|
||||||
|
seenKeys.reserve(cssFiles.size());
|
||||||
|
size_t skippedDuplicates = 0;
|
||||||
|
|
||||||
|
// No cache yet - parse CSS files
|
||||||
|
for (size_t cssIndex = 0; cssIndex < cssFiles.size(); cssIndex++) {
|
||||||
|
const auto& cssPath = cssFiles[cssIndex];
|
||||||
|
const uint64_t dedupKey = dedupKeys[cssIndex];
|
||||||
|
if (dedupKey != 0) {
|
||||||
|
if (std::find(seenKeys.begin(), seenKeys.end(), dedupKey) != seenKeys.end()) {
|
||||||
|
skippedDuplicates++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenKeys.push_back(dedupKey);
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||||
|
|
||||||
|
// Check heap before parsing - CSS parsing allocates heavily
|
||||||
|
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_HEAP_FOR_CSS_PARSING) {
|
||||||
|
LOG_ERR("EBP", "Insufficient heap for CSS parsing (%u bytes free, need %zu), skipping: %s", freeHeap,
|
||||||
|
MIN_HEAP_FOR_CSS_PARSING, cssPath.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check CSS file size before decompressing - skip files that are too large
|
||||||
|
size_t cssFileSize = 0;
|
||||||
|
if (getItemSize(cssPath, &cssFileSize)) {
|
||||||
|
if (cssFileSize > MAX_CSS_FILE_SIZE) {
|
||||||
|
LOG_ERR("EBP", "CSS file too large (%zu bytes > %zu max), skipping: %s", cssFileSize, MAX_CSS_FILE_SIZE,
|
||||||
|
cssPath.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CSS file to temp location
|
||||||
|
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||||
|
HalFile tempCssFile;
|
||||||
|
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||||
|
LOG_ERR("EBP", "Could not create temp CSS file");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||||
|
LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str());
|
||||||
|
// Explicitly close() file before calling Storage.remove()
|
||||||
|
tempCssFile.close();
|
||||||
|
Storage.remove(tmpCssPath.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Explicitly close() file before reopening for reading
|
||||||
|
tempCssFile.close();
|
||||||
|
|
||||||
|
// Parse the CSS file
|
||||||
|
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||||
|
LOG_ERR("EBP", "Could not open temp CSS file for reading");
|
||||||
|
Storage.remove(tmpCssPath.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cssParser->loadFromStream(tempCssFile);
|
||||||
|
// Explicitly close() file before calling Storage.remove()
|
||||||
|
tempCssFile.close();
|
||||||
|
Storage.remove(tmpCssPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to cache for next time
|
||||||
|
if (!cssParser->saveToCache()) {
|
||||||
|
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files (%zu identical duplicates skipped)", cssParser->ruleCount(),
|
||||||
|
cssFiles.size(), skippedDuplicates);
|
||||||
|
cssParser->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// load in the meta data for the epub file
|
||||||
|
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||||
|
LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str());
|
||||||
|
|
||||||
|
// Initialize spine/TOC cache
|
||||||
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||||
|
cssParser.reset(new CssParser(cachePath));
|
||||||
|
|
||||||
|
// Try to load existing cache first
|
||||||
|
if (bookMetadataCache->load()) {
|
||||||
|
if (!skipLoadingCss) {
|
||||||
|
// Rebuild CSS cache when missing or when cache version changed (loadFromCache removes stale file)
|
||||||
|
if (!cssParser->hasCache() || !cssParser->loadFromCache()) {
|
||||||
|
LOG_DBG("EBP", "CSS rules cache missing or stale, attempting to parse CSS files");
|
||||||
|
cssParser->deleteCache();
|
||||||
|
|
||||||
|
BookMetadataCache::BookMetadata cachedMetadata = bookMetadataCache->coreMetadata;
|
||||||
|
if (!parseContentOpf(cachedMetadata, /*writeSpineEntries=*/false)) {
|
||||||
|
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
||||||
|
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||||
|
} else {
|
||||||
|
discoverCssFilesFromZip();
|
||||||
|
}
|
||||||
|
bookMetadataCache.reset();
|
||||||
|
parseCssFiles();
|
||||||
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
if (!bookMetadataCache->load()) {
|
||||||
|
LOG_ERR("EBP", "Failed to reload cache after CSS rebuild");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Invalidate section caches so they are rebuilt with the new CSS
|
||||||
|
Storage.removeDir((cachePath + "/sections").c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Release the resolved CSS rule map: it is only needed transiently while building
|
||||||
|
// section caches, and createSectionFile reloads it from cache on demand. Holding it
|
||||||
|
// resident pins tens of KB for the whole reading session (more on warm resume into
|
||||||
|
// an already-cached chapter, where createSectionFile never runs to clear it).
|
||||||
|
cssParser->clear();
|
||||||
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't load from cache above and we aren't allowed to build, fail now
|
||||||
|
if (!buildIfMissing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache doesn't exist or is invalid, build it
|
||||||
|
LOG_DBG("EBP", "Cache not found, building spine/TOC cache");
|
||||||
|
setupCacheDir();
|
||||||
|
|
||||||
|
const uint32_t indexingStart = millis();
|
||||||
|
|
||||||
|
// Begin building cache - stream entries to disk immediately
|
||||||
|
if (!bookMetadataCache->beginWrite()) {
|
||||||
|
LOG_ERR("EBP", "Could not begin writing cache");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPF Pass
|
||||||
|
const uint32_t opfStart = millis();
|
||||||
|
BookMetadataCache::BookMetadata bookMetadata;
|
||||||
|
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||||
|
LOG_ERR("EBP", "Could not begin writing content.opf pass");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseContentOpf(bookMetadata)) {
|
||||||
|
LOG_ERR("EBP", "Could not parse content.opf");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
discoverCssFilesFromZip();
|
||||||
|
if (!bookMetadataCache->endContentOpfPass()) {
|
||||||
|
LOG_ERR("EBP", "Could not end writing content.opf pass");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart);
|
||||||
|
|
||||||
|
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||||
|
const uint32_t tocStart = millis();
|
||||||
|
if (!bookMetadataCache->beginTocPass()) {
|
||||||
|
LOG_ERR("EBP", "Could not begin writing toc pass");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tocParsed = false;
|
||||||
|
|
||||||
|
// Try EPUB 3 nav document first (preferred)
|
||||||
|
if (!tocNavItem.empty()) {
|
||||||
|
LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document");
|
||||||
|
tocParsed = parseTocNavFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to NCX if nav parsing failed or wasn't available
|
||||||
|
if (!tocParsed && !tocNcxItem.empty()) {
|
||||||
|
LOG_DBG("EBP", "Falling back to NCX TOC");
|
||||||
|
tocParsed = parseTocNcxFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tocParsed) {
|
||||||
|
LOG_ERR("EBP", "Warning: Could not parse any TOC format");
|
||||||
|
// Continue anyway - book will work without TOC
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache->endTocPass()) {
|
||||||
|
LOG_ERR("EBP", "Could not end writing toc pass");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart);
|
||||||
|
|
||||||
|
// Close the cache files
|
||||||
|
if (!bookMetadataCache->endWrite()) {
|
||||||
|
LOG_ERR("EBP", "Could not end writing cache");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final book.bin
|
||||||
|
const uint32_t buildStart = millis();
|
||||||
|
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||||
|
LOG_ERR("EBP", "Could not update mappings and sizes");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart);
|
||||||
|
LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart);
|
||||||
|
|
||||||
|
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||||
|
LOG_DBG("EBP", "Could not cleanup tmp files - ignoring");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipLoadingCss) {
|
||||||
|
// Parse CSS before reloading book.bin to leave more heap for CSS rule-table growth.
|
||||||
|
bookMetadataCache.reset();
|
||||||
|
parseCssFiles();
|
||||||
|
Storage.removeDir((cachePath + "/sections").c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the cache from disk so it's in the correct state
|
||||||
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
if (!bookMetadataCache->load()) {
|
||||||
|
LOG_ERR("EBP", "Failed to reload cache after writing");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::clearCache() const {
|
||||||
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
|
LOG_DBG("EPB", "Cache does not exist, no action needed");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.removeDir(cachePath.c_str())) {
|
||||||
|
LOG_ERR("EPB", "Failed to clear cache");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("EPB", "Cache cleared successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Epub::setupCacheDir() const {
|
||||||
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.mkdir(cachePath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||||
|
|
||||||
|
const std::string& Epub::getPath() const { return filepath; }
|
||||||
|
|
||||||
|
const std::string& Epub::getTitle() const {
|
||||||
|
static std::string blank;
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->coreMetadata.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Epub::getAuthor() const {
|
||||||
|
static std::string blank;
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->coreMetadata.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Epub::getLanguage() const {
|
||||||
|
static std::string blank;
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->coreMetadata.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||||
|
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
|
||||||
|
return cachePath + "/" + coverFileName + ".bmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::generateCoverBmp(bool cropped) const {
|
||||||
|
// Already generated, return true
|
||||||
|
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
|
if (coverImageHref.empty()) {
|
||||||
|
LOG_ERR("EBP", "No known cover image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FsHelpers::hasJpgExtension(coverImageHref)) {
|
||||||
|
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||||
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
|
HalFile coverJpg;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
|
// Explicitly close() file before reopening for reading
|
||||||
|
coverJpg.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile coverBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||||
|
// Explicitly close() files before calling Storage.remove()
|
||||||
|
coverJpg.close();
|
||||||
|
coverBmp.close();
|
||||||
|
Storage.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||||
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FsHelpers::hasPngExtension(coverImageHref)) {
|
||||||
|
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||||
|
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||||
|
|
||||||
|
HalFile coverPng;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||||
|
// Explicitly close() file before reopening for reading
|
||||||
|
coverPng.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile coverBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
|
||||||
|
// Explicitly close() files before calling Storage.remove()
|
||||||
|
coverPng.close();
|
||||||
|
coverBmp.close();
|
||||||
|
Storage.remove(coverPngTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
|
||||||
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(int height) const {
|
||||||
|
// Already generated, return true
|
||||||
|
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
|
if (coverImageHref.empty()) {
|
||||||
|
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||||
|
} else if (FsHelpers::hasJpgExtension(coverImageHref)) {
|
||||||
|
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||||
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
|
HalFile coverJpg;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
|
// Explicitly close() file before reopening for reading
|
||||||
|
coverJpg.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile thumbBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||||
|
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)
|
||||||
|
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);
|
||||||
|
// Explicitly close() files before calling Storage.remove()
|
||||||
|
coverJpg.close();
|
||||||
|
thumbBmp.close();
|
||||||
|
Storage.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image");
|
||||||
|
Storage.remove(getThumbBmpPath(height).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
} else if (FsHelpers::hasPngExtension(coverImageHref)) {
|
||||||
|
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||||
|
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||||
|
|
||||||
|
HalFile coverPng;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||||
|
// Explicitly close() file before reopening for reading
|
||||||
|
coverPng.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile thumbBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||||
|
int THUMB_TARGET_HEIGHT = height;
|
||||||
|
const bool success =
|
||||||
|
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
||||||
|
// Explicitly close() files before calling Storage.remove()
|
||||||
|
coverPng.close();
|
||||||
|
thumbBmp.close();
|
||||||
|
Storage.remove(coverPngTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
|
||||||
|
Storage.remove(getThumbBmpPath(height).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
} else {
|
||||||
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write an empty bmp file to avoid generation attempts in the future
|
||||||
|
HalFile thumbBmp;
|
||||||
|
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||||
|
if (itemHref.empty()) {
|
||||||
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
|
|
||||||
|
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||||
|
if (!content) {
|
||||||
|
LOG_DBG("EBP", "Failed to read item %s", path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize,
|
||||||
|
const bool allowEarlyStop) const {
|
||||||
|
if (itemHref.empty()) {
|
||||||
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
|
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize, allowEarlyStop);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::extractItemToFile(const std::string& itemHref, const std::string& destPath) const {
|
||||||
|
HalFile out;
|
||||||
|
if (!Storage.openFileForWrite("EBP", destPath, out)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool ok = readItemContentsToStream(itemHref, out, 4096);
|
||||||
|
out.flush();
|
||||||
|
out.close();
|
||||||
|
if (!ok) {
|
||||||
|
Storage.remove(destPath.c_str());
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
|
||||||
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
|
return ZipFile(filepath).getInflatedFileSize(path.c_str(), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getSpineItemsCount() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return bookMetadataCache->getSpineCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
LOG_ERR("EBP", "getSpineItem called but cache not loaded");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||||
|
LOG_ERR("EBP", "getSpineItem index:%d is out of range", spineIndex);
|
||||||
|
return bookMetadataCache->getSpineEntry(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->getSpineEntry(spineIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
LOG_DBG("EBP", "getTocItem called but cache not loaded");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
|
LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->getTocEntry(tocIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getTocItemsCount() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->getTocCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// work out the section index for a toc index
|
||||||
|
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
LOG_ERR("EBP", "getSpineIndexForTocIndex called but cache not loaded");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
|
LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", tocIndex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||||
|
if (spineIndex < 0) {
|
||||||
|
LOG_DBG("EBP", "Section not found for TOC index %d", tocIndex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; }
|
||||||
|
|
||||||
|
size_t Epub::getBookSize() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getSpineIndexForTextReference() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s",
|
||||||
|
bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||||
|
|
||||||
|
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||||
|
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through spine items to get the correct index matching the text href
|
||||||
|
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||||
|
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||||
|
LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(),
|
||||||
|
i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||||
|
LOG_DBG("EBP", "Section not found for text reference");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate progress in book (returns 0.0-1.0)
|
||||||
|
float Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||||
|
const size_t bookSize = getBookSize();
|
||||||
|
if (bookSize == 0) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||||
|
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||||
|
const float sectionProgSize = currentSpineRead * static_cast<float>(curChapterSize);
|
||||||
|
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
|
||||||
|
return totalProgress / static_cast<float>(bookSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::resolveHrefToSpineIndex(const std::string& href) const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return -1;
|
||||||
|
|
||||||
|
// Split before decoding so escaped '#' characters in filenames stay part of the path.
|
||||||
|
const size_t hashPos = href.find('#');
|
||||||
|
const std::string rawTarget = hashPos != std::string::npos ? href.substr(0, hashPos) : href;
|
||||||
|
const std::string target = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(rawTarget));
|
||||||
|
|
||||||
|
// Same-file reference (anchor-only)
|
||||||
|
if (target.empty()) return -1;
|
||||||
|
|
||||||
|
// Extract just the filename for comparison
|
||||||
|
size_t targetSlash = target.find_last_of('/');
|
||||||
|
std::string targetFilename = (targetSlash != std::string::npos) ? target.substr(targetSlash + 1) : target;
|
||||||
|
|
||||||
|
for (int i = 0; i < getSpineItemsCount(); i++) {
|
||||||
|
const auto& spineHref = getSpineItem(i).href;
|
||||||
|
// Try exact match first
|
||||||
|
if (spineHref == target) return i;
|
||||||
|
// Then filename-only match
|
||||||
|
size_t spineSlash = spineHref.find_last_of('/');
|
||||||
|
std::string spineFilename = (spineSlash != std::string::npos) ? spineHref.substr(spineSlash + 1) : spineHref;
|
||||||
|
if (spineFilename == targetFilename) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Print.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
#include "Epub/css/CssParser.h"
|
||||||
|
|
||||||
|
class ZipFile;
|
||||||
|
|
||||||
|
class Epub {
|
||||||
|
// the ncx file (EPUB 2)
|
||||||
|
std::string tocNcxItem;
|
||||||
|
// the nav file (EPUB 3)
|
||||||
|
std::string tocNavItem;
|
||||||
|
// where is the EPUBfile?
|
||||||
|
std::string filepath;
|
||||||
|
// the base path for items in the EPUB file
|
||||||
|
std::string contentBasePath;
|
||||||
|
// Uniq cache key based on filepath
|
||||||
|
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 writeSpineEntries = true);
|
||||||
|
bool parseTocNcxFile() const;
|
||||||
|
bool parseTocNavFile() const;
|
||||||
|
void discoverCssFilesFromZip();
|
||||||
|
void parseCssFiles() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||||
|
// create a cache key based on the filepath
|
||||||
|
cachePath = cacheDir + "/epub_" + std::to_string(std::hash<std::string>{}(this->filepath));
|
||||||
|
}
|
||||||
|
~Epub() = default;
|
||||||
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
|
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||||
|
bool clearCache() const;
|
||||||
|
void setupCacheDir() const;
|
||||||
|
const std::string& getCachePath() const;
|
||||||
|
const std::string& getPath() const;
|
||||||
|
const std::string& getTitle() const;
|
||||||
|
const std::string& getAuthor() const;
|
||||||
|
const std::string& getLanguage() const;
|
||||||
|
std::string getCoverBmpPath(bool cropped = false) const;
|
||||||
|
bool generateCoverBmp(bool cropped = false) const;
|
||||||
|
std::string getThumbBmpPath() 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,
|
||||||
|
bool allowEarlyStop = false) const;
|
||||||
|
// Extract an item to a file on SD. On failure the partial file is removed.
|
||||||
|
bool extractItemToFile(const std::string& itemHref, const std::string& destPath) const;
|
||||||
|
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||||
|
BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const;
|
||||||
|
BookMetadataCache::TocEntry getTocItem(int tocIndex) const;
|
||||||
|
int getSpineItemsCount() const;
|
||||||
|
int getTocItemsCount() const;
|
||||||
|
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||||
|
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||||
|
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||||
|
int getSpineIndexForTextReference() const;
|
||||||
|
|
||||||
|
size_t getBookSize() const;
|
||||||
|
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||||
|
CssParser* getCssParser() const { return cssParser.get(); }
|
||||||
|
int resolveHrefToSpineIndex(const std::string& href) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
|
#include <BufferedFile.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
#include <Utf8.h>
|
||||||
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-composed
|
||||||
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on
|
||||||
|
// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash.
|
||||||
|
constexpr size_t BUILD_IO_BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
// Entry (de)serializers, templated so they run over HalFile and the Buffered*
|
||||||
|
// wrappers alike (two instantiations each -- a few hundred bytes of flash, in
|
||||||
|
// exchange for the build path streaming at SD speed instead of per-pod).
|
||||||
|
template <typename F>
|
||||||
|
uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writePod(file, entry.cumulativeSize);
|
||||||
|
serialization::writePod(file, entry.tocIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.title);
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writeString(file, entry.anchor);
|
||||||
|
serialization::writePod(file, entry.level);
|
||||||
|
serialization::writePod(file, entry.spineIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) {
|
||||||
|
BookMetadataCache::SpineEntry entry;
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readPod(file, entry.cumulativeSize);
|
||||||
|
serialization::readPod(file, entry.tocIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
BookMetadataCache::TocEntry readTocEntryFrom(F& file) {
|
||||||
|
BookMetadataCache::TocEntry entry;
|
||||||
|
serialization::readString(file, entry.title);
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readString(file, entry.anchor);
|
||||||
|
serialization::readPod(file, entry.level);
|
||||||
|
serialization::readPod(file, entry.spineIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginWrite() {
|
||||||
|
buildMode = true;
|
||||||
|
spineCount = 0;
|
||||||
|
tocCount = 0;
|
||||||
|
LOG_DBG("BMC", "Entering write mode");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginContentOpfPass() {
|
||||||
|
LOG_DBG("BMC", "Beginning content opf pass");
|
||||||
|
|
||||||
|
// Open spine file for writing
|
||||||
|
if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes.
|
||||||
|
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
|
const bool flushed = !passOut || passOut->flush();
|
||||||
|
passOut.reset();
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
spineFile.close();
|
||||||
|
if (!flushed) {
|
||||||
|
LOG_ERR("BMC", "Failed writing spine tmp file");
|
||||||
|
}
|
||||||
|
return flushed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginTocPass() {
|
||||||
|
LOG_DBG("BMC", "Beginning toc pass");
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||||
|
spineHrefIndex.clear();
|
||||||
|
spineHrefIndex.resize(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[i] = 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;
|
||||||
|
LOG_DBG("BMC", "Using fast index for %d spine items", spineCount);
|
||||||
|
} else {
|
||||||
|
useSpineHrefIndex = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
|
||||||
|
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endTocPass() {
|
||||||
|
const bool flushed = !passOut || passOut->flush();
|
||||||
|
passOut.reset();
|
||||||
|
if (!flushed) {
|
||||||
|
LOG_ERR("BMC", "Failed writing toc tmp file");
|
||||||
|
}
|
||||||
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
|
tocFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
|
||||||
|
spineHrefIndex.clear();
|
||||||
|
spineHrefIndex.shrink_to_fit();
|
||||||
|
useSpineHrefIndex = false;
|
||||||
|
|
||||||
|
return flushed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endWrite() {
|
||||||
|
if (!buildMode) {
|
||||||
|
LOG_DBG("BMC", "endWrite called but not in build mode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMode = false;
|
||||||
|
LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||||
|
// Open all three files, writing to meta, reading from spine and toc
|
||||||
|
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffered streams for the whole build: every access below is sequential per
|
||||||
|
// file, but interleaved ACROSS files, which thrashes SdFat's single shared
|
||||||
|
// sector cache when unbuffered (one 512B SD transaction per 4-byte pod --
|
||||||
|
// measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return.
|
||||||
|
serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||||
|
|
||||||
|
constexpr uint32_t headerASize =
|
||||||
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||||
|
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
||||||
|
sizeof(uint32_t) * 5;
|
||||||
|
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||||
|
const uint32_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
|
// Header A
|
||||||
|
serialization::writePod(bookOut, BOOK_CACHE_VERSION);
|
||||||
|
serialization::writePod(bookOut, lutOffset);
|
||||||
|
serialization::writePod(bookOut, spineCount);
|
||||||
|
serialization::writePod(bookOut, tocCount);
|
||||||
|
// Metadata
|
||||||
|
serialization::writeString(bookOut, metadata.title);
|
||||||
|
serialization::writeString(bookOut, metadata.author);
|
||||||
|
serialization::writeString(bookOut, metadata.language);
|
||||||
|
serialization::writeString(bookOut, metadata.coverItemHref);
|
||||||
|
serialization::writeString(bookOut, metadata.textReferenceHref);
|
||||||
|
|
||||||
|
// Loop through spine entries, writing LUT positions
|
||||||
|
spineIn.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
const uint32_t pos = spineIn.position();
|
||||||
|
readSpineEntryFrom(spineIn);
|
||||||
|
serialization::writePod(bookOut, pos + lutOffset + lutSize);
|
||||||
|
}
|
||||||
|
// Total size of the spine tmp file: entries land in book.bin after the toc LUT
|
||||||
|
// and the full spine block, so toc LUT positions are offset by it.
|
||||||
|
const auto spineBytes = static_cast<uint32_t>(spineIn.position());
|
||||||
|
|
||||||
|
// Loop through toc entries, writing LUT positions
|
||||||
|
tocIn.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
const uint32_t pos = tocIn.position();
|
||||||
|
readTocEntryFrom(tocIn);
|
||||||
|
serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||||
|
tocIn.seek(0);
|
||||||
|
for (int j = 0; j < tocCount; j++) {
|
||||||
|
auto tocEntry = readTocEntryFrom(tocIn);
|
||||||
|
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()) {
|
||||||
|
LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
|
||||||
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
tocFile.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::deque<uint32_t> spineSizes;
|
||||||
|
bool useBatchSizes = false;
|
||||||
|
|
||||||
|
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||||
|
LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
|
||||||
|
|
||||||
|
std::deque<ZipFile::SizeTarget> targets;
|
||||||
|
targets.resize(spineCount);
|
||||||
|
|
||||||
|
spineIn.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto entry = readSpineEntryFrom(spineIn);
|
||||||
|
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[i] = 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);
|
||||||
|
LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", matched, spineCount);
|
||||||
|
|
||||||
|
targets.clear();
|
||||||
|
targets.shrink_to_fit();
|
||||||
|
|
||||||
|
useBatchSizes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cumSize = 0;
|
||||||
|
spineIn.seek(0);
|
||||||
|
int lastSpineTocIndex = -1;
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntryFrom(spineIn);
|
||||||
|
|
||||||
|
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
|
||||||
|
if (spineEntry.tocIndex == -1) {
|
||||||
|
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
|
||||||
|
spineEntry.href.c_str());
|
||||||
|
spineEntry.tocIndex = lastSpineTocIndex;
|
||||||
|
}
|
||||||
|
lastSpineTocIndex = spineEntry.tocIndex;
|
||||||
|
|
||||||
|
size_t itemSize = 0;
|
||||||
|
if (useBatchSizes) {
|
||||||
|
itemSize = spineSizes[i];
|
||||||
|
if (itemSize == 0) {
|
||||||
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
|
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
|
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cumSize += itemSize;
|
||||||
|
spineEntry.cumulativeSize = cumSize;
|
||||||
|
|
||||||
|
// Write out spine data to book.bin
|
||||||
|
writeSpineEntryTo(bookOut, spineEntry);
|
||||||
|
}
|
||||||
|
// Close opened zip file
|
||||||
|
zip.close();
|
||||||
|
|
||||||
|
// Loop through toc entries from toc file writing to book.bin
|
||||||
|
tocIn.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
auto tocEntry = readTocEntryFrom(tocIn);
|
||||||
|
writeTocEntryTo(bookOut, tocEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool written = bookOut.flush();
|
||||||
|
|
||||||
|
// Explicit close() required: member variables persist beyond function scope
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
tocFile.close();
|
||||||
|
|
||||||
|
if (!written) {
|
||||||
|
// A short write (card full/removed) would leave a truncated book.bin that
|
||||||
|
// still passes the version check on load; remove it so the next open rebuilds.
|
||||||
|
LOG_ERR("BMC", "Failed writing book.bin, removing truncated file");
|
||||||
|
Storage.remove((cachePath + bookBinFile).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("BMC", "Successfully built book.bin");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||||
|
const auto spineBinFile = cachePath + tmpSpineBinFile;
|
||||||
|
if (Storage.exists(spineBinFile.c_str())) {
|
||||||
|
Storage.remove(spineBinFile.c_str());
|
||||||
|
}
|
||||||
|
const auto tocBinFile = cachePath + tmpTocBinFile;
|
||||||
|
if (Storage.exists(tocBinFile.c_str())) {
|
||||||
|
Storage.remove(tocBinFile.c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||||
|
return writeSpineEntryTo(file, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||||
|
return writeTocEntryTo(file, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||||
|
// this is because in this function we're marking positions of the items
|
||||||
|
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||||
|
if (!buildMode || !spineFile) {
|
||||||
|
LOG_DBG("BMC", "createSpineEntry called but not in build mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpineEntry entry(href, 0, -1);
|
||||||
|
if (passOut) {
|
||||||
|
writeSpineEntryTo(*passOut, entry);
|
||||||
|
} else {
|
||||||
|
writeSpineEntry(spineFile, entry);
|
||||||
|
}
|
||||||
|
spineCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||||
|
const uint8_t level) {
|
||||||
|
if (!buildMode || !tocFile || !spineFile) {
|
||||||
|
LOG_DBG("BMC", "createTocEntry called but not in build mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", 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) {
|
||||||
|
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
||||||
|
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||||
|
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||||
|
if (passOut) {
|
||||||
|
writeTocEntryTo(*passOut, entry);
|
||||||
|
} else {
|
||||||
|
writeTocEntry(tocFile, entry);
|
||||||
|
}
|
||||||
|
tocCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::load() {
|
||||||
|
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(bookFile, version);
|
||||||
|
if (version != BOOK_CACHE_VERSION) {
|
||||||
|
LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::readPod(bookFile, lutOffset);
|
||||||
|
serialization::readPod(bookFile, spineCount);
|
||||||
|
serialization::readPod(bookFile, tocCount);
|
||||||
|
|
||||||
|
serialization::readString(bookFile, coreMetadata.title);
|
||||||
|
serialization::readString(bookFile, coreMetadata.author);
|
||||||
|
serialization::readString(bookFile, coreMetadata.language);
|
||||||
|
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||||
|
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||||
|
LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to spine LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
|
||||||
|
uint32_t spineEntryPos;
|
||||||
|
serialization::readPod(bookFile, spineEntryPos);
|
||||||
|
bookFile.seek(spineEntryPos);
|
||||||
|
return readSpineEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||||
|
LOG_ERR("BMC", "getTocEntry index %d out of range", index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to TOC LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
|
||||||
|
uint32_t tocEntryPos;
|
||||||
|
serialization::readPod(bookFile, tocEntryPos);
|
||||||
|
bookFile.seek(tocEntryPos);
|
||||||
|
return readTocEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||||
|
return readSpineEntryFrom(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <BufferedFile.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class BookMetadataCache {
|
||||||
|
public:
|
||||||
|
struct BookMetadata {
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
std::string language;
|
||||||
|
std::string coverItemHref;
|
||||||
|
std::string textReferenceHref;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SpineEntry {
|
||||||
|
std::string href;
|
||||||
|
uint32_t cumulativeSize;
|
||||||
|
int16_t tocIndex;
|
||||||
|
|
||||||
|
SpineEntry() : cumulativeSize(0), tocIndex(-1) {}
|
||||||
|
SpineEntry(std::string href, const uint32_t cumulativeSize, const int16_t tocIndex)
|
||||||
|
: href(std::move(href)), cumulativeSize(cumulativeSize), tocIndex(tocIndex) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TocEntry {
|
||||||
|
std::string title;
|
||||||
|
std::string href;
|
||||||
|
std::string anchor;
|
||||||
|
uint8_t level;
|
||||||
|
int16_t spineIndex;
|
||||||
|
|
||||||
|
TocEntry() : level(0), spineIndex(-1) {}
|
||||||
|
TocEntry(std::string title, std::string href, std::string anchor, const uint8_t level, const int16_t spineIndex)
|
||||||
|
: title(std::move(title)),
|
||||||
|
href(std::move(href)),
|
||||||
|
anchor(std::move(anchor)),
|
||||||
|
level(level),
|
||||||
|
spineIndex(spineIndex) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string cachePath;
|
||||||
|
uint32_t lutOffset;
|
||||||
|
uint16_t spineCount;
|
||||||
|
uint16_t tocCount;
|
||||||
|
bool loaded;
|
||||||
|
bool buildMode;
|
||||||
|
|
||||||
|
HalFile bookFile;
|
||||||
|
// Temp file handles during build
|
||||||
|
HalFile spineFile;
|
||||||
|
HalFile tocFile;
|
||||||
|
// Buffers the per-entry tmp-file writes during the OPF/TOC passes: those
|
||||||
|
// writes interleave with zip-inflate SD reads, and unbuffered they thrash
|
||||||
|
// SdFat's shared sector cache (one 512B transaction per 4-byte pod). One
|
||||||
|
// wrapper serves whichever pass is active (spine, then toc).
|
||||||
|
std::unique_ptr<serialization::BufferedFileWriter> passOut;
|
||||||
|
|
||||||
|
// 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::deque<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(HalFile& file, const SpineEntry& entry) const;
|
||||||
|
uint32_t writeTocEntry(HalFile& file, const TocEntry& entry) const;
|
||||||
|
SpineEntry readSpineEntry(HalFile& file) const;
|
||||||
|
TocEntry readTocEntry(HalFile& file) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BookMetadata coreMetadata;
|
||||||
|
|
||||||
|
explicit BookMetadataCache(std::string cachePath)
|
||||||
|
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||||
|
~BookMetadataCache() = default;
|
||||||
|
|
||||||
|
// Building phase (stream to disk immediately)
|
||||||
|
bool beginWrite();
|
||||||
|
bool beginContentOpfPass();
|
||||||
|
void createSpineEntry(const std::string& href);
|
||||||
|
bool endContentOpfPass();
|
||||||
|
bool beginTocPass();
|
||||||
|
void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
||||||
|
bool endTocPass();
|
||||||
|
bool endWrite();
|
||||||
|
bool cleanupTmpFiles() const;
|
||||||
|
|
||||||
|
// Post-processing to update mappings and sizes
|
||||||
|
bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata);
|
||||||
|
|
||||||
|
// Reading phase (read mode)
|
||||||
|
bool load();
|
||||||
|
SpineEntry getSpineEntry(int index);
|
||||||
|
TocEntry getTocEntry(int index);
|
||||||
|
int getSpineCount() const { return spineCount; }
|
||||||
|
int getTocCount() const { return tocCount; }
|
||||||
|
bool isLoaded() const { return loaded; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#include "Page.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
template <typename Predicate>
|
||||||
|
void renderFilteredPageElements(const std::vector<std::shared_ptr<PageElement>>& elements, GfxRenderer& renderer,
|
||||||
|
const int fontId, const int xOffset, const int yOffset, Predicate&& predicate) {
|
||||||
|
for (const auto& element : elements) {
|
||||||
|
if (predicate(*element)) {
|
||||||
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PageLine::serialize(HalFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
|
||||||
|
// serialize TextBlock pointed to by PageLine
|
||||||
|
return block->serialize(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageLine> PageLine::deserialize(HalFile& file) {
|
||||||
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
serialization::readPod(file, xPos);
|
||||||
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
|
auto tb = TextBlock::deserialize(file);
|
||||||
|
if (!tb) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: null TextBlock");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* line = new (std::nothrow) PageLine(std::move(tb), xPos, yPos);
|
||||||
|
if (!line) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: could not allocate PageLine");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::unique_ptr<PageLine>(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
// Images don't use fontId or text rendering
|
||||||
|
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
|
||||||
|
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PageImage::serialize(HalFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
|
||||||
|
// serialize ImageBlock
|
||||||
|
return imageBlock->serialize(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageImage> PageImage::deserialize(HalFile& file) {
|
||||||
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
serialization::readPod(file, xPos);
|
||||||
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
|
auto ib = ImageBlock::deserialize(file);
|
||||||
|
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageHorizontalRule::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
(void)fontId;
|
||||||
|
if (width == 0 || thickness == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset, thickness, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PageHorizontalRule::serialize(HalFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
serialization::writePod(file, width);
|
||||||
|
serialization::writePod(file, thickness);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(HalFile& file) {
|
||||||
|
int16_t xPos = 0;
|
||||||
|
int16_t yPos = 0;
|
||||||
|
uint16_t width = 0;
|
||||||
|
uint8_t thickness = 0;
|
||||||
|
serialization::readPod(file, xPos);
|
||||||
|
serialization::readPod(file, yPos);
|
||||||
|
serialization::readPod(file, width);
|
||||||
|
serialization::readPod(file, thickness);
|
||||||
|
|
||||||
|
if (width == 0 || thickness == 0) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: invalid horizontal rule metadata (width=%u thickness=%u)", width,
|
||||||
|
thickness);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* rule = new (std::nothrow) PageHorizontalRule(width, thickness, xPos, yPos);
|
||||||
|
if (!rule) {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: could not allocate PageHorizontalRule");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::unique_ptr<PageHorizontalRule>(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
|
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, [](const PageElement&) { return true; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
|
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset,
|
||||||
|
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
|
||||||
|
const int yOffset) const {
|
||||||
|
for (const auto& element : elements) {
|
||||||
|
if (element->getTag() == TAG_PageImage) {
|
||||||
|
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
|
||||||
|
} else {
|
||||||
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Page::serialize(HalFile& file) const {
|
||||||
|
const uint16_t count = elements.size();
|
||||||
|
serialization::writePod(file, count);
|
||||||
|
|
||||||
|
for (const auto& el : elements) {
|
||||||
|
// Use getTag() method to determine type
|
||||||
|
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||||
|
|
||||||
|
if (!el->serialize(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize footnotes (clamp to MAX_FOOTNOTES_PER_PAGE to match addFootnote/deserialize limits)
|
||||||
|
const uint16_t fnCount = std::min<uint16_t>(footnotes.size(), MAX_FOOTNOTES_PER_PAGE);
|
||||||
|
serialization::writePod(file, fnCount);
|
||||||
|
for (uint16_t i = 0; i < fnCount; i++) {
|
||||||
|
const auto& fn = footnotes[i];
|
||||||
|
if (file.write(fn.number, sizeof(fn.number)) != sizeof(fn.number) ||
|
||||||
|
file.write(fn.href, sizeof(fn.href)) != sizeof(fn.href)) {
|
||||||
|
LOG_ERR("PGE", "Failed to write footnote");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Page> Page::deserialize(HalFile& file) {
|
||||||
|
auto page = std::unique_ptr<Page>(new Page());
|
||||||
|
|
||||||
|
uint16_t count;
|
||||||
|
serialization::readPod(file, count);
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
uint8_t tag;
|
||||||
|
serialization::readPod(file, tag);
|
||||||
|
|
||||||
|
if (tag == TAG_PageLine) {
|
||||||
|
auto pl = PageLine::deserialize(file);
|
||||||
|
if (!pl) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
page->elements.push_back(std::move(pl));
|
||||||
|
} else if (tag == TAG_PageImage) {
|
||||||
|
auto pi = PageImage::deserialize(file);
|
||||||
|
if (!pi) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
page->elements.push_back(std::move(pi));
|
||||||
|
} else if (tag == TAG_PageHorizontalRule) {
|
||||||
|
auto rule = PageHorizontalRule::deserialize(file);
|
||||||
|
if (!rule) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
page->elements.push_back(std::move(rule));
|
||||||
|
} else {
|
||||||
|
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize footnotes
|
||||||
|
uint16_t fnCount;
|
||||||
|
serialization::readPod(file, fnCount);
|
||||||
|
if (fnCount > MAX_FOOTNOTES_PER_PAGE) {
|
||||||
|
LOG_ERR("PGE", "Invalid footnote count %u", fnCount);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
page->footnotes.resize(fnCount);
|
||||||
|
for (uint16_t i = 0; i < fnCount; i++) {
|
||||||
|
auto& entry = page->footnotes[i];
|
||||||
|
if (file.read(entry.number, sizeof(entry.number)) != sizeof(entry.number) ||
|
||||||
|
file.read(entry.href, sizeof(entry.href)) != sizeof(entry.href)) {
|
||||||
|
LOG_ERR("PGE", "Failed to read footnote %u", i);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
entry.number[sizeof(entry.number) - 1] = '\0';
|
||||||
|
entry.href[sizeof(entry.href) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "FootnoteEntry.h"
|
||||||
|
#include "blocks/ImageBlock.h"
|
||||||
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
|
enum PageElementTag : uint8_t {
|
||||||
|
TAG_PageLine = 1,
|
||||||
|
TAG_PageImage = 2,
|
||||||
|
TAG_PageHorizontalRule = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// represents something that has been added to a page
|
||||||
|
class PageElement {
|
||||||
|
public:
|
||||||
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||||
|
virtual ~PageElement() = default;
|
||||||
|
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||||
|
virtual bool serialize(HalFile& file) = 0;
|
||||||
|
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||||
|
};
|
||||||
|
|
||||||
|
// a line from a block element
|
||||||
|
class PageLine final : public PageElement {
|
||||||
|
std::shared_ptr<TextBlock> block;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
|
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
bool serialize(HalFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||||
|
static std::unique_ptr<PageLine> deserialize(HalFile& file);
|
||||||
|
};
|
||||||
|
|
||||||
|
// New PageImage class
|
||||||
|
class PageImage final : public PageElement {
|
||||||
|
std::shared_ptr<ImageBlock> imageBlock;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
|
||||||
|
bool serialize(HalFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
|
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
||||||
|
const ImageBlock& getImageBlock() const { return *imageBlock; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class PageHorizontalRule final : public PageElement {
|
||||||
|
uint16_t width;
|
||||||
|
uint8_t thickness;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageHorizontalRule(uint16_t width, uint8_t thickness, const int16_t xPos, const int16_t yPos)
|
||||||
|
: PageElement(xPos, yPos), width(width), thickness(thickness) {}
|
||||||
|
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
bool serialize(HalFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageHorizontalRule; }
|
||||||
|
static std::unique_ptr<PageHorizontalRule> deserialize(HalFile& file);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
public:
|
||||||
|
// the list of block index and line numbers on this page
|
||||||
|
std::vector<std::shared_ptr<PageElement>> elements;
|
||||||
|
std::vector<FootnoteEntry> footnotes;
|
||||||
|
static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = 16;
|
||||||
|
|
||||||
|
void addFootnote(const char* number, const char* href) {
|
||||||
|
if (footnotes.size() >= MAX_FOOTNOTES_PER_PAGE) return; // Cap per-page footnotes
|
||||||
|
FootnoteEntry entry;
|
||||||
|
strncpy(entry.number, number, sizeof(entry.number) - 1);
|
||||||
|
entry.number[sizeof(entry.number) - 1] = '\0';
|
||||||
|
strncpy(entry.href, href, sizeof(entry.href) - 1);
|
||||||
|
entry.href[sizeof(entry.href) - 1] = '\0';
|
||||||
|
footnotes.push_back(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
bool serialize(HalFile& file) const;
|
||||||
|
static std::unique_ptr<Page> deserialize(HalFile& file);
|
||||||
|
|
||||||
|
// Check if page contains any images (used to force full refresh)
|
||||||
|
bool hasImages() const {
|
||||||
|
return std::any_of(elements.begin(), elements.end(),
|
||||||
|
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasImagesNeedingDecode() const {
|
||||||
|
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
|
||||||
|
return element->getTag() == TAG_PageImage &&
|
||||||
|
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bounding box of all images on the page (union of image rects)
|
||||||
|
// Returns false if no images. Coordinates are relative to page origin.
|
||||||
|
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||||
|
bool found = false;
|
||||||
|
int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN;
|
||||||
|
for (const auto& el : elements) {
|
||||||
|
if (el->getTag() == TAG_PageImage) {
|
||||||
|
const auto& img = static_cast<const PageImage&>(*el);
|
||||||
|
int16_t x = img.xPos;
|
||||||
|
int16_t y = img.yPos;
|
||||||
|
int16_t right = x + img.getImageBlock().getWidth();
|
||||||
|
int16_t bottom = y + img.getImageBlock().getHeight();
|
||||||
|
minX = std::min(minX, x);
|
||||||
|
minY = std::min(minY, y);
|
||||||
|
maxX = std::max(maxX, right);
|
||||||
|
maxY = std::max(maxY, bottom);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
outX = minX;
|
||||||
|
outY = minY;
|
||||||
|
outW = maxX - minX;
|
||||||
|
outH = maxY - minY;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "blocks/BlockStyle.h"
|
||||||
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
class ParsedText {
|
||||||
|
std::vector<std::string> words;
|
||||||
|
std::vector<EpdFontFamily::Style> wordStyles;
|
||||||
|
std::vector<bool> wordContinues; // true = word attaches to previous with no break
|
||||||
|
std::vector<bool> wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined
|
||||||
|
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
|
||||||
|
BlockStyle blockStyle;
|
||||||
|
bool extraParagraphSpacing;
|
||||||
|
bool hyphenationEnabled;
|
||||||
|
bool focusReadingEnabled;
|
||||||
|
bool isNaturalAlign;
|
||||||
|
bool hasRtlWord;
|
||||||
|
std::vector<std::string> reorderedWordsScratch;
|
||||||
|
std::vector<EpdFontFamily::Style> reorderedStylesScratch;
|
||||||
|
std::vector<uint16_t> reorderedWidthsScratch;
|
||||||
|
std::vector<bool> reorderedContinuesScratch;
|
||||||
|
std::vector<bool> reorderedNoSpaceBeforeScratch;
|
||||||
|
std::vector<bool> reorderedFocusSuffixScratch;
|
||||||
|
std::vector<uint16_t> visualOrderScratch;
|
||||||
|
|
||||||
|
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
|
||||||
|
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
std::vector<bool>& noSpaceBeforeVec);
|
||||||
|
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||||
|
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||||
|
std::vector<bool>& noSpaceBeforeVec);
|
||||||
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||||
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||||
|
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||||
|
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
|
||||||
|
const std::vector<size_t>& lineBreakIndices,
|
||||||
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
||||||
|
int fontId);
|
||||||
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||||
|
const bool focusReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle())
|
||||||
|
: blockStyle(blockStyle),
|
||||||
|
extraParagraphSpacing(extraParagraphSpacing),
|
||||||
|
hyphenationEnabled(hyphenationEnabled),
|
||||||
|
focusReadingEnabled(focusReadingEnabled),
|
||||||
|
isNaturalAlign(false),
|
||||||
|
hasRtlWord(false) {}
|
||||||
|
~ParsedText() = default;
|
||||||
|
|
||||||
|
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,
|
||||||
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||||
|
bool includeLastLine = true);
|
||||||
|
};
|
||||||
@@ -0,0 +1,933 @@
|
|||||||
|
#include "Section.h"
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include "Epub/css/CssParser.h"
|
||||||
|
#include "Page.h"
|
||||||
|
#include "hyphenation/Hyphenator.h"
|
||||||
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// v28: text decoration bits now include line-through in serialized wordStyles.
|
||||||
|
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||||
|
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||||
|
// v30: Arabic shaping changed both drawing and measurement (getTextAdvanceX now
|
||||||
|
// measures the shaped visual text); cached word positions from v29 no longer
|
||||||
|
// match what drawText renders.
|
||||||
|
// v32: ImageBlock serializes the book-internal source href after the cache path
|
||||||
|
// (lazy extraction: images are header-probed at build time and extracted on
|
||||||
|
// first render).
|
||||||
|
constexpr uint8_t SECTION_FILE_VERSION = 32;
|
||||||
|
// Written into the version field while a build is in progress; patched to
|
||||||
|
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||||
|
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||||
|
// as unknown and clears -- so an incomplete file is never mistaken for a valid one.
|
||||||
|
constexpr uint8_t SECTION_FILE_INCOMPLETE_VERSION = 0;
|
||||||
|
// Written when a build is suspended partway (reader exited or device slept mid-build).
|
||||||
|
// The file carries valid pages 0..pageCount-1, all LUTs, and a trailer with the parse
|
||||||
|
// watermark (bytesConsumed, totalBytes) appended after the li LUT. loadSectionFile
|
||||||
|
// accepts it so a resume shows those pages instantly; the reader extends it by
|
||||||
|
// rebuilding in the background. Uses the same header layout as SECTION_FILE_VERSION,
|
||||||
|
// so finalized files are untouched by this feature; older firmware treats the sentinel
|
||||||
|
// as an unknown version and rebuilds, which is a safe downgrade.
|
||||||
|
// MUST change in lockstep with SECTION_FILE_VERSION: the sentinel IS the partial's
|
||||||
|
// format version, so a stale-format partial otherwise passes the header check and
|
||||||
|
// only fails (noisily, via the block-decode error path) when a page is loaded.
|
||||||
|
// Derived so the pairing can't be forgotten: 0xFE for v28, 0xFD for v29, ...
|
||||||
|
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE - (SECTION_FILE_VERSION - 28);
|
||||||
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||||
|
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||||
|
sizeof(uint32_t) + sizeof(uint32_t);
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
|
||||||
|
// constructed/destroyed where the parser's full definition is visible.
|
||||||
|
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
|
: epub(epub),
|
||||||
|
spineIndex(spineIndex),
|
||||||
|
renderer(renderer),
|
||||||
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
|
|
||||||
|
// Suspend any in-progress build so every section.reset() / navigation / sleep path
|
||||||
|
// persists the pages already laid out as a partial .bin instead of discarding them
|
||||||
|
// (no-op once a build has completed or never started).
|
||||||
|
Section::~Section() { suspendBuild(); }
|
||||||
|
|
||||||
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t position = file.position();
|
||||||
|
if (!page->serialize(file)) {
|
||||||
|
LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
LOG_DBG("SCT", "Page %d processed", builtPageCount_);
|
||||||
|
|
||||||
|
builtPageCount_++;
|
||||||
|
// pageCount is the pages available to read: a rebuild over a partial only raises it
|
||||||
|
// once it has laid out more pages than the partial already covers.
|
||||||
|
if (builtPageCount_ > pageCount) {
|
||||||
|
pageCount = builtPageCount_;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bool embeddedStyle, const uint8_t imageRendering,
|
||||||
|
const bool focusReadingEnabled) {
|
||||||
|
if (!file) {
|
||||||
|
LOG_DBG("SCT", "File not open for writing header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
||||||
|
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||||
|
"Header size mismatch");
|
||||||
|
// Written as the incomplete sentinel; finalizeBuild() patches it to
|
||||||
|
// SECTION_FILE_VERSION as the last step, committing the file.
|
||||||
|
serialization::writePod(file, SECTION_FILE_INCOMPLETE_VERSION);
|
||||||
|
serialization::writePod(file, fontId);
|
||||||
|
serialization::writePod(file, lineCompression);
|
||||||
|
serialization::writePod(file, extraParagraphSpacing);
|
||||||
|
serialization::writePod(file, paragraphAlignment);
|
||||||
|
serialization::writePod(file, viewportWidth);
|
||||||
|
serialization::writePod(file, viewportHeight);
|
||||||
|
serialization::writePod(file, hyphenationEnabled);
|
||||||
|
serialization::writePod(file, embeddedStyle);
|
||||||
|
serialization::writePod(file, imageRendering);
|
||||||
|
serialization::writePod(file, focusReadingEnabled);
|
||||||
|
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
|
||||||
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
|
||||||
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
|
||||||
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for paragraph LUT offset (patched later)
|
||||||
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for li LUT offset (patched later)
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
|
const uint8_t imageRendering, const bool focusReadingEnabled) {
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match parameters
|
||||||
|
bool filePartial = false;
|
||||||
|
{
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(file, version);
|
||||||
|
if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) {
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
file.close();
|
||||||
|
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||||
|
clearCache();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
|
int fileFontId;
|
||||||
|
uint16_t fileViewportWidth, fileViewportHeight;
|
||||||
|
float fileLineCompression;
|
||||||
|
bool fileExtraParagraphSpacing;
|
||||||
|
uint8_t fileParagraphAlignment;
|
||||||
|
bool fileHyphenationEnabled;
|
||||||
|
bool fileEmbeddedStyle;
|
||||||
|
uint8_t fileImageRendering;
|
||||||
|
bool fileFocusReadingEnabled;
|
||||||
|
serialization::readPod(file, fileFontId);
|
||||||
|
serialization::readPod(file, fileLineCompression);
|
||||||
|
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||||
|
serialization::readPod(file, fileParagraphAlignment);
|
||||||
|
serialization::readPod(file, fileViewportWidth);
|
||||||
|
serialization::readPod(file, fileViewportHeight);
|
||||||
|
serialization::readPod(file, fileHyphenationEnabled);
|
||||||
|
serialization::readPod(file, fileEmbeddedStyle);
|
||||||
|
serialization::readPod(file, fileImageRendering);
|
||||||
|
serialization::readPod(file, fileFocusReadingEnabled);
|
||||||
|
|
||||||
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||||
|
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||||
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||||
|
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||||
|
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
|
||||||
|
file.close();
|
||||||
|
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||||
|
clearCache();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::readPod(file, pageCount);
|
||||||
|
|
||||||
|
if (filePartial) {
|
||||||
|
// A partial's pageCount is the watermark of a suspended build. Read the watermark
|
||||||
|
// trailer (appended after the li LUT) so estimatedTotalPages can extrapolate.
|
||||||
|
uint32_t liLutOffset = 0;
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||||
|
serialization::readPod(file, liLutOffset);
|
||||||
|
const uint32_t trailerOffset = liLutOffset + static_cast<uint32_t>(pageCount) * sizeof(uint16_t);
|
||||||
|
const bool trailerValid =
|
||||||
|
pageCount > 0 && liLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size();
|
||||||
|
if (!trailerValid) {
|
||||||
|
file.close();
|
||||||
|
LOG_ERR("SCT", "Deserialization failed: malformed partial section");
|
||||||
|
clearCache();
|
||||||
|
pageCount = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file.seek(trailerOffset);
|
||||||
|
serialization::readPod(file, partialBytesConsumed_);
|
||||||
|
serialization::readPod(file, partialTotalBytes_);
|
||||||
|
partial_ = true;
|
||||||
|
partialPageCount_ = pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
file.close();
|
||||||
|
LOG_DBG("SCT", "Deserialization succeeded: %d pages%s", pageCount, filePartial ? " (partial)" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||||
|
bool Section::clearCache() const {
|
||||||
|
const std::string tmpBin = binTmpPath();
|
||||||
|
if (Storage.exists(tmpBin.c_str())) {
|
||||||
|
Storage.remove(tmpBin.c_str());
|
||||||
|
}
|
||||||
|
if (!Storage.exists(filePath.c_str())) {
|
||||||
|
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.remove(filePath.c_str())) {
|
||||||
|
LOG_ERR("SCT", "Failed to clear cache");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("SCT", "Cache cleared successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bool embeddedStyle,
|
||||||
|
const uint8_t imageRendering, const bool focusReadingEnabled,
|
||||||
|
const std::function<void()>& popupFn) {
|
||||||
|
// One-shot build: start, then lay out the whole section in a single pass.
|
||||||
|
if (!startBuild(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
|
||||||
|
hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled, popupFn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!buildSomeMore(0)) { // 0 = build to completion
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return buildComplete_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::startBuild(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight,
|
||||||
|
const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering,
|
||||||
|
const bool focusReadingEnabled, const std::function<void()>& popupFn) {
|
||||||
|
if (build_) {
|
||||||
|
LOG_ERR("SCT", "startBuild called while a build is already active");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buildComplete_ = false;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
// Pages from a loaded partial stay readable (from filePath) while this build writes
|
||||||
|
// to the tmp .bin, so availability never drops below the partial's watermark.
|
||||||
|
pageCount = partial_ ? partialPageCount_ : 0;
|
||||||
|
|
||||||
|
// Remove a stale tmp .bin from a crash-interrupted build; this build recreates it.
|
||||||
|
{
|
||||||
|
const std::string staleTmp = binTmpPath();
|
||||||
|
if (Storage.exists(staleTmp.c_str())) {
|
||||||
|
Storage.remove(staleTmp.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
|
const auto htmlDir = epub->getCachePath() + "/html";
|
||||||
|
const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html";
|
||||||
|
const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
|
|
||||||
|
// Create cache directory if it doesn't exist
|
||||||
|
{
|
||||||
|
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||||
|
Storage.mkdir(sectionsDir.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse the previously unzipped HTML if we already have it. The unzipped HTML is keyed only on the
|
||||||
|
// book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation
|
||||||
|
// that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip
|
||||||
|
// inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so
|
||||||
|
// even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a
|
||||||
|
// reopen skip the multi-second inflate. If htmlPath exists it is known-complete.
|
||||||
|
const bool reusedHtml = Storage.exists(htmlPath.c_str());
|
||||||
|
bool htmlCached = reusedHtml;
|
||||||
|
if (reusedHtml) {
|
||||||
|
LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str());
|
||||||
|
} else {
|
||||||
|
Storage.mkdir(htmlDir.c_str());
|
||||||
|
|
||||||
|
// Retry logic for SD card timing issues
|
||||||
|
bool streamed = false;
|
||||||
|
uint32_t fileSize = 0;
|
||||||
|
for (int attempt = 0; attempt < 3 && !streamed; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||||
|
delay(50); // Brief delay before retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any incomplete file from previous attempt before retrying
|
||||||
|
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile tmpHtml;
|
||||||
|
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Larger chunks mean far fewer SD writes inflating the HTML; a 1KB chunk turned a 584KB
|
||||||
|
// single-spine novel into ~570 tiny writes (multi-second). 8KB keeps the transient buffers
|
||||||
|
// small while cutting the write count 8x.
|
||||||
|
streamed = epub->readItemContentsToStream(localPath, tmpHtml, 8192);
|
||||||
|
fileSize = tmpHtml.size();
|
||||||
|
// Explicitly close() file before calling Storage.remove()
|
||||||
|
tmpHtml.close();
|
||||||
|
|
||||||
|
// If streaming failed, remove the incomplete file immediately
|
||||||
|
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamed) {
|
||||||
|
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||||
|
|
||||||
|
// Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are
|
||||||
|
// valid regardless of whether the layout build finishes, so reopening (even a window-only spine
|
||||||
|
// that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp.
|
||||||
|
if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) {
|
||||||
|
htmlCached = true;
|
||||||
|
} else {
|
||||||
|
LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Storage.openFileForWrite("SCT", binTmpPath(), file)) {
|
||||||
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Header is written with the incomplete-version sentinel; finalizeBuild() commits it.
|
||||||
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
||||||
|
|
||||||
|
auto ctx = makeUniqueNoThrow<BuildContext>();
|
||||||
|
if (!ctx) {
|
||||||
|
LOG_ERR("SCT", "OOM: BuildContext");
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// htmlCached == "htmlPath is the live cache" (reused, or just promoted). finalizeBuild/abandonBuild
|
||||||
|
// then leave the cached HTML alone; only an un-promoted temp (rename failed) is theirs to clean up.
|
||||||
|
ctx->reusedHtml = htmlCached;
|
||||||
|
ctx->htmlPath = htmlPath;
|
||||||
|
ctx->tmpHtmlPath = tmpHtmlPath;
|
||||||
|
ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath;
|
||||||
|
|
||||||
|
// Derive the content base directory and image cache path prefix for the parser
|
||||||
|
const size_t lastSlash = localPath.find_last_of('/');
|
||||||
|
ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||||
|
ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||||
|
|
||||||
|
if (embeddedStyle) {
|
||||||
|
ctx->cssParser = epub->getCssParser();
|
||||||
|
if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
|
||||||
|
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect TOC anchors for this spine so the parser can insert page breaks at chapter boundaries
|
||||||
|
std::vector<std::string> tocAnchors;
|
||||||
|
const int startTocIndex = epub->getTocIndexForSpineIndex(spineIndex);
|
||||||
|
if (startTocIndex >= 0) {
|
||||||
|
for (int i = startTocIndex; i < epub->getTocItemsCount(); i++) {
|
||||||
|
auto entry = epub->getTocItem(i);
|
||||||
|
if (entry.spineIndex != spineIndex) break;
|
||||||
|
if (!entry.anchor.empty()) {
|
||||||
|
tocAnchors.push_back(std::move(entry.anchor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parser stores the path/contentBase/imageBasePath by reference, so they must
|
||||||
|
// live in the BuildContext (which outlives the parser). The page-complete callback
|
||||||
|
// captures the BuildContext pointer to append to its in-RAM LUT; build_ owns the
|
||||||
|
// context for the parser's whole lifetime.
|
||||||
|
BuildContext* ctxPtr = ctx.get();
|
||||||
|
ctx->parser = makeUniqueNoThrow<ChapterHtmlSlimParser>(
|
||||||
|
epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
|
||||||
|
viewportWidth, viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
||||||
|
[this, ctxPtr](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
||||||
|
ctxPtr->lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
||||||
|
},
|
||||||
|
embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn,
|
||||||
|
ctxPtr->cssParser);
|
||||||
|
if (!ctx->parser) {
|
||||||
|
LOG_ERR("SCT", "OOM: ChapterHtmlSlimParser");
|
||||||
|
if (ctx->cssParser) ctx->cssParser->clear();
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
|
build_ = std::move(ctx);
|
||||||
|
|
||||||
|
if (!build_->parser->beginParse()) {
|
||||||
|
LOG_ERR("SCT", "Failed to begin parse");
|
||||||
|
abandonBuild();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
build_->totalBytes = build_->parser->parseTotalBytes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::buildSomeMore(const int maxPages) {
|
||||||
|
if (!build_ || !build_->parser) {
|
||||||
|
LOG_ERR("SCT", "buildSomeMore with no active build");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial,
|
||||||
|
// pageCount stays pinned at the partial's watermark until the build passes it, which
|
||||||
|
// would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark.
|
||||||
|
const int startCount = builtPageCount_;
|
||||||
|
for (;;) {
|
||||||
|
const auto status = build_->parser->parseStep();
|
||||||
|
if (status == ChapterHtmlSlimParser::ParseStatus::Error) {
|
||||||
|
LOG_ERR("SCT", "Parse error during incremental build");
|
||||||
|
abandonBuild();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (status == ChapterHtmlSlimParser::ParseStatus::Done) {
|
||||||
|
return finalizeBuild();
|
||||||
|
}
|
||||||
|
// ParseStatus::More: yield once we've laid out the requested number of pages.
|
||||||
|
if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) {
|
||||||
|
build_->bytesConsumed = build_->parser->parseBytesConsumed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::hasHtmlCache() const {
|
||||||
|
const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html";
|
||||||
|
return Storage.exists(htmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::findAnchorDuringBuild(const std::string& anchor) const {
|
||||||
|
if (!build_ || !build_->parser) return std::nullopt;
|
||||||
|
for (const auto& [key, page] : build_->parser->getAnchors()) {
|
||||||
|
if (key == anchor) return page;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::findAnchor(const std::string& anchor) const {
|
||||||
|
if (const auto page = findAnchorDuringBuild(anchor)) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
// Fall back to the on-disk anchor map: a finalized section, or a partial whose map
|
||||||
|
// covers everything up to its watermark (nullopt past it -- build further and retry).
|
||||||
|
return getPageForAnchor(anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Section::estimatedTotalPages() const {
|
||||||
|
// Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA
|
||||||
|
// damping is needed. Also the best guess while a rebuild is running but hasn't laid out
|
||||||
|
// enough pages yet to extrapolate from its own progress.
|
||||||
|
const auto partialEstimate = [this]() -> uint16_t {
|
||||||
|
if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) {
|
||||||
|
return pageCount;
|
||||||
|
}
|
||||||
|
const uint64_t est = static_cast<uint64_t>(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_;
|
||||||
|
if (est <= pageCount) return pageCount;
|
||||||
|
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!build_) {
|
||||||
|
return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact
|
||||||
|
}
|
||||||
|
const uint32_t consumed = build_->bytesConsumed;
|
||||||
|
const uint32_t total = build_->totalBytes;
|
||||||
|
if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate();
|
||||||
|
|
||||||
|
// Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This
|
||||||
|
// re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses
|
||||||
|
// dense vs sparse regions of the chapter.
|
||||||
|
const uint64_t raw = static_cast<uint64_t>(builtPageCount_) * total / consumed;
|
||||||
|
|
||||||
|
// Damp that jitter with an exponential moving average. Step it once per build advance (keyed on
|
||||||
|
// bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how
|
||||||
|
// often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so
|
||||||
|
// the average settles onto the true count (and finalizeBuild then returns the exact pageCount).
|
||||||
|
constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle
|
||||||
|
if (build_->smoothedEstimate <= 0) {
|
||||||
|
build_->smoothedEstimate = static_cast<float>(raw); // seed on the first estimate
|
||||||
|
} else if (consumed != build_->smoothedAtConsumed) {
|
||||||
|
build_->smoothedEstimate += ALPHA * (static_cast<float>(raw) - build_->smoothedEstimate);
|
||||||
|
}
|
||||||
|
build_->smoothedAtConsumed = consumed;
|
||||||
|
|
||||||
|
const uint64_t est = static_cast<uint64_t>(build_->smoothedEstimate + 0.5f);
|
||||||
|
if (est <= pageCount) return pageCount; // never fewer than the pages already available
|
||||||
|
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built
|
||||||
|
// page count and table offsets, stamp `version` as the commit point, then swap the tmp
|
||||||
|
// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer
|
||||||
|
// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate
|
||||||
|
// the total page count. The parser must still be alive (anchors are read from it).
|
||||||
|
// On failure the tmp is removed and any pre-existing file at filePath is left intact.
|
||||||
|
bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) {
|
||||||
|
const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||||
|
|
||||||
|
const auto failCommit = [this]() {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint32_t lutOffset = file.position();
|
||||||
|
for (const auto& entry : build_->lut) {
|
||||||
|
if (entry.fileOffset == 0) {
|
||||||
|
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||||
|
return failCommit();
|
||||||
|
}
|
||||||
|
serialization::writePod(file, entry.fileOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a
|
||||||
|
// partial, skip anchors that landed on the incomplete trailing page the suspend drops.
|
||||||
|
const uint32_t anchorMapOffset = file.position();
|
||||||
|
const auto& anchors = build_->parser->getAnchors();
|
||||||
|
uint16_t anchorCount = 0;
|
||||||
|
for (const auto& [anchor, page] : anchors) {
|
||||||
|
if (!asPartial || page < builtPageCount_) anchorCount++;
|
||||||
|
}
|
||||||
|
serialization::writePod(file, anchorCount);
|
||||||
|
for (const auto& [anchor, page] : anchors) {
|
||||||
|
if (asPartial && page >= builtPageCount_) continue;
|
||||||
|
serialization::writeString(file, anchor);
|
||||||
|
serialization::writePod(file, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t paragraphLutOffset = file.position();
|
||||||
|
serialization::writePod(file, static_cast<uint16_t>(build_->lut.size()));
|
||||||
|
for (const auto& entry : build_->lut) {
|
||||||
|
serialization::writePod(file, entry.paragraphIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
|
||||||
|
for (const auto& entry : build_->lut) {
|
||||||
|
serialization::writePod(file, entry.listItemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asPartial) {
|
||||||
|
// Watermark trailer, located on load as liLutOffset + pageCount * sizeof(uint16_t).
|
||||||
|
serialization::writePod(file, bytesConsumed);
|
||||||
|
serialization::writePod(file, totalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch header with the built page count and section offsets...
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(builtPageCount_));
|
||||||
|
serialization::writePod(file, builtPageCount_);
|
||||||
|
serialization::writePod(file, lutOffset);
|
||||||
|
serialization::writePod(file, anchorMapOffset);
|
||||||
|
serialization::writePod(file, paragraphLutOffset);
|
||||||
|
serialization::writePod(file, liLutFileOffset);
|
||||||
|
// ...then commit by overwriting the sentinel version with the real one. Writing the
|
||||||
|
// version last makes it the commit point: a crash before here leaves version 0.
|
||||||
|
file.seek(0);
|
||||||
|
serialization::writePod(file, version);
|
||||||
|
// Explicit close() required: member variable persists beyond function scope
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Swap into place. A crash between remove and rename loses the old file but keeps a
|
||||||
|
// fully-committed tmp; the next build just removes it and rebuilds.
|
||||||
|
if (Storage.exists(filePath.c_str())) {
|
||||||
|
Storage.remove(filePath.c_str());
|
||||||
|
}
|
||||||
|
if (!Storage.rename(binTmpPath().c_str(), filePath.c_str())) {
|
||||||
|
LOG_ERR("SCT", "Failed to move built section into place");
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::finalizeBuild() {
|
||||||
|
// Flush the trailing page (emits the last page via the completePageFn into the LUT).
|
||||||
|
build_->parser->finishParse();
|
||||||
|
|
||||||
|
if (!build_->reusedHtml) {
|
||||||
|
// Parse succeeded: promote the freshly unzipped HTML to the persistent cache so future
|
||||||
|
// rebuilds skip zip inflation. If promotion fails, drop the temp -- the build still succeeded.
|
||||||
|
if (!Storage.rename(build_->tmpHtmlPath.c_str(), build_->htmlPath.c_str())) {
|
||||||
|
LOG_DBG("SCT", "Failed to promote HTML cache, removing temp");
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0);
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
build_.reset();
|
||||||
|
if (!committed) {
|
||||||
|
// commitBuildFile removed filePath before the failed swap, so nothing valid remains.
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buildComplete_ = true;
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = builtPageCount_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::suspendBuild() {
|
||||||
|
if (!build_) return;
|
||||||
|
|
||||||
|
// Only worth persisting if this build produced pages a pre-existing partial doesn't
|
||||||
|
// already cover; otherwise keep the older (bigger) partial and just drop the tmp.
|
||||||
|
const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_);
|
||||||
|
|
||||||
|
bool committed = false;
|
||||||
|
if (worthKeeping) {
|
||||||
|
// Capture the parse watermark and commit before tearing the parser down (the anchor
|
||||||
|
// map is read from it). The incomplete trailing page is intentionally not flushed:
|
||||||
|
// only fully laid-out pages are persisted, and the rebuild re-derives the rest.
|
||||||
|
const uint32_t consumed = static_cast<uint32_t>(build_->parser->parseBytesConsumed());
|
||||||
|
committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes);
|
||||||
|
if (committed) {
|
||||||
|
partial_ = true;
|
||||||
|
partialPageCount_ = builtPageCount_;
|
||||||
|
partialBytesConsumed_ = consumed;
|
||||||
|
partialTotalBytes_ = build_->totalBytes;
|
||||||
|
LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build_->parser) build_->parser->abortParse();
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
if (!committed && file) {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
}
|
||||||
|
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
build_.reset();
|
||||||
|
buildComplete_ = false;
|
||||||
|
pageCount = partial_ ? partialPageCount_ : 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::abandonBuild() {
|
||||||
|
if (!build_) return;
|
||||||
|
if (build_->parser) build_->parser->abortParse();
|
||||||
|
if (build_->cssParser) build_->cssParser->clear();
|
||||||
|
if (file) {
|
||||||
|
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||||
|
file.close();
|
||||||
|
Storage.remove(binTmpPath().c_str());
|
||||||
|
}
|
||||||
|
// A parse error would recur against the same HTML, so drop any partial too -- resuming
|
||||||
|
// from it would just re-enter the failing build every open.
|
||||||
|
if (Storage.exists(filePath.c_str())) {
|
||||||
|
Storage.remove(filePath.c_str());
|
||||||
|
}
|
||||||
|
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||||
|
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
build_.reset();
|
||||||
|
buildComplete_ = false;
|
||||||
|
partial_ = false;
|
||||||
|
partialPageCount_ = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
builtPageCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Page> Section::loadPageDuringBuild(const int page) {
|
||||||
|
if (!build_ || page < 0 || page >= static_cast<int>(build_->lut.size()) || !file) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const uint32_t pos = build_->lut[page].fileOffset;
|
||||||
|
if (pos == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// The .bin is open O_RDWR for the build. Read the already-written page, then restore
|
||||||
|
// the write cursor so the next onPageComplete keeps appending where it left off.
|
||||||
|
const uint32_t writePos = file.position();
|
||||||
|
file.seek(pos);
|
||||||
|
auto p = Page::deserialize(file);
|
||||||
|
file.seek(writePos);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a page from the committed file at filePath (finalized section or partial from a
|
||||||
|
// previous session). Uses a local handle so it is safe while a build holds the member
|
||||||
|
// `file` open on the tmp .bin.
|
||||||
|
std::unique_ptr<Page> Section::loadPageAt(const int page) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
|
||||||
|
uint32_t lutOffset;
|
||||||
|
serialization::readPod(f, lutOffset);
|
||||||
|
f.seek(lutOffset + sizeof(uint32_t) * page);
|
||||||
|
uint32_t pagePos;
|
||||||
|
serialization::readPod(f, pagePos);
|
||||||
|
f.seek(pagePos);
|
||||||
|
|
||||||
|
return Page::deserialize(f);
|
||||||
|
// No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Page> Section::loadPage(const int page) {
|
||||||
|
if (page < 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (build_ && page < static_cast<int>(build_->lut.size())) {
|
||||||
|
return loadPageDuringBuild(page);
|
||||||
|
}
|
||||||
|
// Not (yet) in the active build: serve from the file on disk -- a finalized section,
|
||||||
|
// or a partial from a previous session whose pages the rebuild hasn't reached again.
|
||||||
|
const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount);
|
||||||
|
if (page >= onDisk) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return loadPageAt(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Section::getTextFromSectionFile() {
|
||||||
|
std::string fullText;
|
||||||
|
auto p = loadPage(currentPage);
|
||||||
|
if (p) {
|
||||||
|
for (const auto& el : p->elements) {
|
||||||
|
if (el->getTag() == TAG_PageLine) {
|
||||||
|
const auto& line = static_cast<const PageLine&>(*el);
|
||||||
|
if (line.getBlock()) {
|
||||||
|
const auto& block = *line.getBlock();
|
||||||
|
for (uint16_t i = 0; i < block.wordCount(); i++) {
|
||||||
|
if (!fullText.empty()) fullText += " ";
|
||||||
|
fullText += block.wordText(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fullText;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::getCachedPageCount() const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t fileSize = f.size();
|
||||||
|
if (fileSize < HEADER_SIZE) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only a finalized section's count is the chapter total; a partial's count is just the
|
||||||
|
// suspended build's watermark, which would skew progress mapping. Callers fall back to
|
||||||
|
// their own estimates.
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(f, version);
|
||||||
|
if (version != SECTION_FILE_VERSION) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
||||||
|
uint16_t count;
|
||||||
|
serialization::readPod(f, count);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t fileSize = f.size();
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 3);
|
||||||
|
uint32_t anchorMapOffset;
|
||||||
|
serialization::readPod(f, anchorMapOffset);
|
||||||
|
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(anchorMapOffset);
|
||||||
|
uint16_t count;
|
||||||
|
serialization::readPod(f, count);
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
std::string key;
|
||||||
|
uint16_t page;
|
||||||
|
serialization::readString(f, key);
|
||||||
|
serialization::readPod(f, page);
|
||||||
|
if (key == anchor) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::getPageForParagraphIndex(const uint16_t pIndex) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t fileSize = f.size();
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||||
|
uint32_t paragraphLutOffset;
|
||||||
|
serialization::readPod(f, paragraphLutOffset);
|
||||||
|
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(paragraphLutOffset);
|
||||||
|
uint16_t count;
|
||||||
|
serialization::readPod(f, count);
|
||||||
|
if (count == 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t lutEnd = paragraphLutOffset + sizeof(uint16_t) + count * sizeof(uint16_t);
|
||||||
|
if (lutEnd > fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t resultPage = count - 1;
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
uint16_t pagePIdx;
|
||||||
|
serialization::readPod(f, pagePIdx);
|
||||||
|
if (pagePIdx >= pIndex) {
|
||||||
|
resultPage = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::getParagraphIndexForPage(const uint16_t page) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t fileSize = f.size();
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||||
|
uint32_t paragraphLutOffset;
|
||||||
|
serialization::readPod(f, paragraphLutOffset);
|
||||||
|
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(paragraphLutOffset);
|
||||||
|
uint16_t count;
|
||||||
|
serialization::readPod(f, count);
|
||||||
|
if (count == 0 || page >= count) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t entryEnd = paragraphLutOffset + sizeof(uint16_t) + (page + 1) * sizeof(uint16_t);
|
||||||
|
if (entryEnd > fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(paragraphLutOffset + sizeof(uint16_t) + page * sizeof(uint16_t));
|
||||||
|
uint16_t pIdx;
|
||||||
|
serialization::readPod(f, pIdx);
|
||||||
|
return pIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::getPageForListItemIndex(const uint16_t liIndex) const {
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t fileSize = f.size();
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||||
|
uint32_t liLutOffset;
|
||||||
|
serialization::readPod(f, liLutOffset);
|
||||||
|
if (liLutOffset == 0 || liLutOffset >= fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The li LUT shares count with the paragraph LUT; read count from paragraphLutOffset
|
||||||
|
f.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||||
|
uint32_t paragraphLutOffset;
|
||||||
|
serialization::readPod(f, paragraphLutOffset);
|
||||||
|
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(paragraphLutOffset);
|
||||||
|
uint16_t count;
|
||||||
|
serialization::readPod(f, count);
|
||||||
|
if (count == 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t lutEnd = liLutOffset + count * sizeof(uint16_t);
|
||||||
|
if (lutEnd > fileSize) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.seek(liLutOffset);
|
||||||
|
uint16_t resultPage = count - 1;
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
uint16_t pageLiIdx;
|
||||||
|
serialization::readPod(f, pageLiIdx);
|
||||||
|
if (pageLiIdx >= liIndex) {
|
||||||
|
resultPage = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultPage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Epub.h"
|
||||||
|
|
||||||
|
class Page;
|
||||||
|
class GfxRenderer;
|
||||||
|
class ChapterHtmlSlimParser;
|
||||||
|
class CssParser;
|
||||||
|
|
||||||
|
class Section {
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
|
const int spineIndex;
|
||||||
|
GfxRenderer& renderer;
|
||||||
|
std::string filePath;
|
||||||
|
HalFile file;
|
||||||
|
|
||||||
|
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||||
|
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
||||||
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
|
// Page-offset table entry, kept in RAM while an incremental build is running so
|
||||||
|
// already-built pages can be located in the partially-written .bin.
|
||||||
|
struct PageLutEntry {
|
||||||
|
uint32_t fileOffset;
|
||||||
|
uint16_t paragraphIndex;
|
||||||
|
uint16_t listItemIndex;
|
||||||
|
};
|
||||||
|
// Held only while an incremental build is in progress (see startBuild). Carries the
|
||||||
|
// live parser plus the strings it references (the parser stores them by reference)
|
||||||
|
// and the in-RAM page-offset table.
|
||||||
|
struct BuildContext {
|
||||||
|
std::unique_ptr<ChapterHtmlSlimParser> parser;
|
||||||
|
std::vector<PageLutEntry> lut;
|
||||||
|
std::string parsePath;
|
||||||
|
std::string contentBase;
|
||||||
|
std::string imageBasePath;
|
||||||
|
std::string htmlPath;
|
||||||
|
std::string tmpHtmlPath;
|
||||||
|
bool reusedHtml = false;
|
||||||
|
CssParser* cssParser = nullptr;
|
||||||
|
// HTML byte progress, for estimating the section's total page count while it's still building.
|
||||||
|
uint32_t bytesConsumed = 0;
|
||||||
|
uint32_t totalBytes = 0;
|
||||||
|
// Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its
|
||||||
|
// last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions;
|
||||||
|
// the EMA is stepped once per build advance (not per redraw) to damp that wobble.
|
||||||
|
float smoothedEstimate = 0;
|
||||||
|
uint32_t smoothedAtConsumed = 0;
|
||||||
|
};
|
||||||
|
std::unique_ptr<BuildContext> build_;
|
||||||
|
bool buildComplete_ = false;
|
||||||
|
// Pages laid out by the active build (== build_->lut.size()). Distinct from pageCount,
|
||||||
|
// which is the pages *available to read* and also counts a loaded partial file's pages.
|
||||||
|
uint16_t builtPageCount_ = 0;
|
||||||
|
// A partial section file (suspended build from a previous session) is loaded at filePath.
|
||||||
|
// Its pages 0..partialPageCount_-1 are readable while a rebuild extends past them.
|
||||||
|
bool partial_ = false;
|
||||||
|
uint16_t partialPageCount_ = 0;
|
||||||
|
// Parse watermark from the partial's trailer, for estimating the total page count.
|
||||||
|
uint32_t partialBytesConsumed_ = 0;
|
||||||
|
uint32_t partialTotalBytes_ = 0;
|
||||||
|
bool finalizeBuild();
|
||||||
|
// Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the
|
||||||
|
// header, stamp the version byte, and swap the tmp .bin over filePath.
|
||||||
|
bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes);
|
||||||
|
// Builds write here and are swapped over filePath only on commit, so a prior
|
||||||
|
// partial/finalized file stays readable while a rebuild is in progress.
|
||||||
|
std::string binTmpPath() const { return filePath + ".part"; }
|
||||||
|
std::unique_ptr<Page> loadPageAt(int page) const;
|
||||||
|
// Read a page already laid out by the in-progress build (page < build LUT size), from
|
||||||
|
// the partially-written tmp .bin without disturbing the build's write cursor.
|
||||||
|
std::unique_ptr<Page> loadPageDuringBuild(int page);
|
||||||
|
|
||||||
|
public:
|
||||||
|
uint16_t pageCount = 0;
|
||||||
|
int currentPage = 0;
|
||||||
|
|
||||||
|
// Constructor and destructor are out-of-line: BuildContext holds a unique_ptr to the
|
||||||
|
// forward-declared ChapterHtmlSlimParser, whose full definition is only visible in the .cpp.
|
||||||
|
explicit Section(const std::shared_ptr<Epub>& epub, int spineIndex, GfxRenderer& renderer);
|
||||||
|
~Section();
|
||||||
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
|
uint8_t imageRendering, bool focusReadingEnabled);
|
||||||
|
bool clearCache() const;
|
||||||
|
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
|
uint8_t imageRendering, bool focusReadingEnabled,
|
||||||
|
const std::function<void()>& popupFn = nullptr);
|
||||||
|
|
||||||
|
// Incremental build: lay out the section a few pages at a time so a large chapter
|
||||||
|
// can show its first page immediately and keep the UI responsive while the rest
|
||||||
|
// builds. createSectionFile() above is the one-shot wrapper over these.
|
||||||
|
// if (!startBuild(...)) fail;
|
||||||
|
// each tick: buildSomeMore(N); render up to pageCount; when isBuildComplete() stop.
|
||||||
|
bool startBuild(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
|
uint8_t imageRendering, bool focusReadingEnabled, const std::function<void()>& popupFn = nullptr);
|
||||||
|
// Lay out up to maxPages more pages (maxPages <= 0 = build to completion). Returns
|
||||||
|
// false on error (the build is abandoned). Sets isBuildComplete() when finished.
|
||||||
|
bool buildSomeMore(int maxPages);
|
||||||
|
bool isBuilding() const { return static_cast<bool>(build_); }
|
||||||
|
bool isBuildComplete() const { return buildComplete_; }
|
||||||
|
// Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based
|
||||||
|
// estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine
|
||||||
|
// is still building, so "page X of Y" / progress don't read off the small build watermark.
|
||||||
|
uint16_t estimatedTotalPages() const;
|
||||||
|
void abandonBuild();
|
||||||
|
// Persist an in-progress build as a partial section file (version sentinel + LUTs +
|
||||||
|
// watermark trailer) instead of discarding it, so the next open of this spine can show
|
||||||
|
// its pages instantly and only rebuild in the background. Called by the destructor, so
|
||||||
|
// any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a
|
||||||
|
// pre-existing partial when it covers more pages than this build reached.
|
||||||
|
void suspendBuild();
|
||||||
|
// True when a partial file was loaded: pageCount is a watermark, not the chapter total.
|
||||||
|
bool isPartial() const { return partial_; }
|
||||||
|
|
||||||
|
// Unified page read: from the active build if it has reached the page, otherwise from
|
||||||
|
// the on-disk file (finalized section, or a partial the rebuild hasn't caught up to).
|
||||||
|
std::unique_ptr<Page> loadPage(int page);
|
||||||
|
|
||||||
|
std::string getTextFromSectionFile();
|
||||||
|
|
||||||
|
// Resolve an anchor from the in-progress build first, then the on-disk anchor map
|
||||||
|
// (covers finalized sections and partials from a previous session).
|
||||||
|
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
|
||||||
|
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
|
||||||
|
bool hasHtmlCache() const;
|
||||||
|
|
||||||
|
// Look up the page number for an anchor id from the section cache file.
|
||||||
|
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// Look up an anchor among the pages built so far by the in-progress build, so an anchor jump
|
||||||
|
// (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the
|
||||||
|
// whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build.
|
||||||
|
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// Get the page count from the section cache file without fully loading it.
|
||||||
|
std::optional<uint16_t> getCachedPageCount() const;
|
||||||
|
|
||||||
|
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
||||||
|
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||||
|
|
||||||
|
// Look up the page number for a running list-item index from the li LUT.
|
||||||
|
std::optional<uint16_t> getPageForListItemIndex(uint16_t liIndex) const;
|
||||||
|
|
||||||
|
// Look up the synthetic paragraph index for the given rendered page.
|
||||||
|
std::optional<uint16_t> getParagraphIndexForPage(uint16_t page) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||||
|
|
||||||
|
// a block of content in the html - either a paragraph or an image
|
||||||
|
class Block {
|
||||||
|
public:
|
||||||
|
virtual ~Block() = default;
|
||||||
|
|
||||||
|
virtual BlockType getType() = 0;
|
||||||
|
virtual bool isEmpty() = 0;
|
||||||
|
virtual void finish() {}
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/css/CssStyle.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlockStyle - Block-level styling properties
|
||||||
|
*/
|
||||||
|
struct BlockStyle {
|
||||||
|
// Upper bound (in em) for any single side's horizontal margin or padding.
|
||||||
|
// Some EPUBs apply huge em-based insets to chapter-opener classes; without a
|
||||||
|
// cap, effectiveWidth collapses to 1-2 words per line and justification dumps
|
||||||
|
// the remaining space into a single gap.
|
||||||
|
static constexpr float MAX_HORIZONTAL_INSET_EM = 2.0f;
|
||||||
|
|
||||||
|
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
|
||||||
|
bool isRtl = false; // true if resolved direction is RTL
|
||||||
|
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
||||||
|
|
||||||
|
// Set when this block was created by a <br> element. Used by startNewTextBlock to inject
|
||||||
|
// a full line-height gap when the <br> block stays empty (section-break use case).
|
||||||
|
// NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
|
||||||
|
bool fromBrElement = false;
|
||||||
|
|
||||||
|
// Combined 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(); }
|
||||||
|
[[nodiscard]] int16_t topInset() const { return marginTop + paddingTop; }
|
||||||
|
[[nodiscard]] int16_t bottomInset() const { return marginBottom + paddingBottom; }
|
||||||
|
|
||||||
|
// Return a copy with bottom margins/padding zeroed out.
|
||||||
|
[[nodiscard]] BlockStyle withoutBottom() const {
|
||||||
|
BlockStyle result = *this;
|
||||||
|
result.marginBottom = 0;
|
||||||
|
result.paddingBottom = 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy with bottom margins/padding collapsed (max) with the source's.
|
||||||
|
// Uses CSS margin collapsing: adjacent parent-child margins resolve to the larger value.
|
||||||
|
[[nodiscard]] BlockStyle addBottom(const BlockStyle& source) const {
|
||||||
|
BlockStyle result = *this;
|
||||||
|
result.marginBottom = std::max(marginBottom, source.marginBottom);
|
||||||
|
result.paddingBottom = static_cast<int16_t>(paddingBottom + source.paddingBottom);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CombineAxis : uint8_t {
|
||||||
|
Horizontal = 1, // margins left/right, padding left/right, text-align, text-indent
|
||||||
|
Vertical = 2, // margins top/bottom, padding top/bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine this style's properties with a child style along the specified axis.
|
||||||
|
// Properties on the other axis are kept from the child unchanged.
|
||||||
|
[[nodiscard]] BlockStyle getCombinedBlockStyle(const BlockStyle& child, CombineAxis axis) const {
|
||||||
|
BlockStyle result = child;
|
||||||
|
|
||||||
|
if (axis == CombineAxis::Horizontal) {
|
||||||
|
result.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
|
||||||
|
result.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
|
||||||
|
result.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
|
||||||
|
result.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
|
||||||
|
if (!child.textIndentDefined && textIndentDefined) {
|
||||||
|
result.textIndent = textIndent;
|
||||||
|
result.textIndentDefined = true;
|
||||||
|
}
|
||||||
|
if (!child.textAlignDefined && textAlignDefined) {
|
||||||
|
result.alignment = alignment;
|
||||||
|
result.textAlignDefined = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.marginTop = std::max(child.marginTop, marginTop);
|
||||||
|
result.marginBottom = std::max(child.marginBottom, marginBottom);
|
||||||
|
result.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
|
||||||
|
result.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction is not axis-specific. Inherit from parent when child doesn't define it.
|
||||||
|
if (!child.directionDefined && directionDefined) {
|
||||||
|
result.isRtl = isRtl;
|
||||||
|
result.directionDefined = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromBrElement is consumed by startNewTextBlock when an empty <br> block
|
||||||
|
// is merged with the following paragraph; never propagate it further.
|
||||||
|
result.fromBrElement = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
const auto maxHorizontalInsetPx = static_cast<int16_t>(emSize * MAX_HORIZONTAL_INSET_EM);
|
||||||
|
// 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 = std::min(cssStyle.marginLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||||
|
blockStyle.marginRight = std::min(cssStyle.marginRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||||
|
|
||||||
|
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
|
||||||
|
blockStyle.paddingLeft = std::min(cssStyle.paddingLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||||
|
blockStyle.paddingRight = std::min(cssStyle.paddingRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||||
|
|
||||||
|
// For textIndent: if it's a percentage we can't resolve (no viewport width),
|
||||||
|
// leave textIndentDefined=false so the space-width fallback in resolveFirstLineIndent() 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;
|
||||||
|
}
|
||||||
|
// RTL direction from CSS/HTML
|
||||||
|
if (cssStyle.hasDirection()) {
|
||||||
|
blockStyle.isRtl = (cssStyle.direction == CssTextDirection::Rtl);
|
||||||
|
blockStyle.directionDefined = true;
|
||||||
|
}
|
||||||
|
return blockStyle;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
#include "ImageBlock.h"
|
||||||
|
|
||||||
|
#include <FontCacheManager.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
#include "Epub/converters/DirectPixelWriter.h"
|
||||||
|
#include "Epub/converters/ImageDecoderFactory.h"
|
||||||
|
|
||||||
|
// Cache file format:
|
||||||
|
// - uint16_t width
|
||||||
|
// - uint16_t height
|
||||||
|
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||||
|
|
||||||
|
ImageBlock::ImageBlock(const std::string& imagePath, const std::string& srcPath, int16_t width, int16_t height)
|
||||||
|
: imagePath(imagePath), srcPath(srcPath), width(width), height(height) {}
|
||||||
|
|
||||||
|
void* ImageBlock::extractCtx = nullptr;
|
||||||
|
ImageBlock::ExtractFn ImageBlock::extractFn = nullptr;
|
||||||
|
|
||||||
|
void ImageBlock::setExtractor(void* ctx, ExtractFn fn) {
|
||||||
|
extractCtx = ctx;
|
||||||
|
extractFn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string getCachePath(const std::string& imagePath) {
|
||||||
|
// Replace extension with .pxc (pixel cache)
|
||||||
|
size_t dotPos = imagePath.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
return imagePath.substr(0, dotPos) + ".pxc";
|
||||||
|
}
|
||||||
|
return imagePath + ".pxc";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
|
||||||
|
uint16_t& cachedHeight) {
|
||||||
|
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int widthDiff = abs(cachedWidth - expectedWidth);
|
||||||
|
const int heightDiff = abs(cachedHeight - expectedHeight);
|
||||||
|
if (widthDiff > 1 || heightDiff > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t bytesPerRow = (cachedWidth + 3) / 4;
|
||||||
|
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
|
||||||
|
return cacheFile.size() >= expectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
|
||||||
|
// record so an image that failed renders its placeholder directly for the rest
|
||||||
|
// of the reader session instead of paying another placeholder refresh and
|
||||||
|
// decode. The reader clears this on entry so transient memory/storage failures
|
||||||
|
// are retried.
|
||||||
|
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
|
||||||
|
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
|
||||||
|
size_t failedImageCount = 0;
|
||||||
|
|
||||||
|
uint64_t imagePathHash(const std::string& path) {
|
||||||
|
uint64_t hash = 14695981039346656037ull;
|
||||||
|
for (const char c : path) {
|
||||||
|
hash ^= static_cast<uint8_t>(c);
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool imageFailedThisSession(const std::string& path) {
|
||||||
|
const uint64_t hash = imagePathHash(path);
|
||||||
|
for (size_t i = 0; i < failedImageCount; i++) {
|
||||||
|
if (failedImageHashes[i] == hash) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rememberImageFailure(const std::string& path) {
|
||||||
|
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
|
||||||
|
failedImageHashes[failedImageCount++] = imagePathHash(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-page-render RAM slot for the pixel cache ----------------------------
|
||||||
|
// The tiled grayscale flow re-renders an image page once for the BW
|
||||||
|
// double-refresh and again for every band of both gray planes, and each pass
|
||||||
|
// re-read the whole .pxc off SD (~100 ms for a full-page image, ~13 passes).
|
||||||
|
// Column clipping cannot reduce the SD traffic: the row stride (~100 B) is
|
||||||
|
// smaller than an SD sector, so every sector is touched regardless of the band
|
||||||
|
// window. Instead the first pass loads the payload into RAM and later passes
|
||||||
|
// render from it. Chunked allocation because a single full-image block (up to
|
||||||
|
// 96 KB) rarely fits the fragmented mid-render heap; each chunk is heap-gated
|
||||||
|
// and any failure falls back to the streaming path unchanged. The reader
|
||||||
|
// releases the slot when the page render completes, so nothing stays resident
|
||||||
|
// across page turns.
|
||||||
|
constexpr size_t PXC_CHUNK_SHIFT = 14; // 16 KB chunks
|
||||||
|
constexpr size_t PXC_CHUNK_SIZE = 1u << PXC_CHUNK_SHIFT;
|
||||||
|
constexpr size_t PXC_MAX_CHUNKS = 6; // 96 KB: a full-screen 2bpp image
|
||||||
|
constexpr size_t PXC_HEAP_RESERVE = 24 * 1024;
|
||||||
|
constexpr size_t PXC_MAX_ALLOC_RESERVE = 8 * 1024;
|
||||||
|
// Rows can straddle a chunk boundary; they are reassembled into a stack
|
||||||
|
// buffer. (screenWidth + 3) / 4 caps at 200 B for an 800px panel.
|
||||||
|
constexpr int PXC_MAX_BYTES_PER_ROW = 208;
|
||||||
|
|
||||||
|
std::unique_ptr<uint8_t[]> pxcChunks[PXC_MAX_CHUNKS];
|
||||||
|
uint64_t pxcSlotHash = 0;
|
||||||
|
uint16_t pxcSlotWidth = 0;
|
||||||
|
uint16_t pxcSlotHeight = 0;
|
||||||
|
|
||||||
|
void releasePxcSlot() {
|
||||||
|
for (auto& chunk : pxcChunks) chunk.reset();
|
||||||
|
pxcSlotHash = 0;
|
||||||
|
pxcSlotWidth = 0;
|
||||||
|
pxcSlotHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* pxcRowPtr(size_t rowStart, int bytesPerRow, uint8_t* tempRow) {
|
||||||
|
const size_t chunk = rowStart >> PXC_CHUNK_SHIFT;
|
||||||
|
const size_t offset = rowStart & (PXC_CHUNK_SIZE - 1);
|
||||||
|
if (offset + bytesPerRow <= PXC_CHUNK_SIZE) {
|
||||||
|
return pxcChunks[chunk].get() + offset;
|
||||||
|
}
|
||||||
|
const size_t firstPart = PXC_CHUNK_SIZE - offset;
|
||||||
|
memcpy(tempRow, pxcChunks[chunk].get() + offset, firstPart);
|
||||||
|
memcpy(tempRow + firstPart, pxcChunks[chunk + 1].get(), bytesPerRow - firstPart);
|
||||||
|
return tempRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheFile is positioned just past the header. True when the slot holds the
|
||||||
|
// full pixel payload for this cache path afterward.
|
||||||
|
bool loadPxcSlot(uint64_t cacheHash, HalFile& cacheFile, uint16_t cachedWidth, uint16_t cachedHeight, int bytesPerRow) {
|
||||||
|
releasePxcSlot();
|
||||||
|
if (bytesPerRow > PXC_MAX_BYTES_PER_ROW) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t remaining = (size_t)bytesPerRow * cachedHeight;
|
||||||
|
const size_t chunkCount = (remaining + PXC_CHUNK_SIZE - 1) >> PXC_CHUNK_SHIFT;
|
||||||
|
if (chunkCount == 0 || chunkCount > PXC_MAX_CHUNKS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < chunkCount; i++) {
|
||||||
|
const size_t want = remaining < PXC_CHUNK_SIZE ? remaining : PXC_CHUNK_SIZE;
|
||||||
|
if (ESP.getFreeHeap() < remaining + PXC_HEAP_RESERVE || ESP.getMaxAllocHeap() < want + PXC_MAX_ALLOC_RESERVE) {
|
||||||
|
releasePxcSlot();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pxcChunks[i] = makeUniqueNoThrow<uint8_t[]>(want);
|
||||||
|
if (!pxcChunks[i] || cacheFile.read(pxcChunks[i].get(), want) != static_cast<int>(want)) {
|
||||||
|
releasePxcSlot();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
remaining -= want;
|
||||||
|
}
|
||||||
|
pxcSlotHash = cacheHash;
|
||||||
|
pxcSlotWidth = cachedWidth;
|
||||||
|
pxcSlotHeight = cachedHeight;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderRowsFromPxcSlot(GfxRenderer& renderer, int x, int y) {
|
||||||
|
const int bytesPerRow = (pxcSlotWidth + 3) / 4;
|
||||||
|
uint8_t tempRow[PXC_MAX_BYTES_PER_ROW];
|
||||||
|
|
||||||
|
DirectPixelWriter pw;
|
||||||
|
pw.init(renderer);
|
||||||
|
|
||||||
|
for (int row = 0; row < pxcSlotHeight; row++) {
|
||||||
|
const uint8_t* rowBuffer = pxcRowPtr((size_t)row * bytesPerRow, bytesPerRow, tempRow);
|
||||||
|
pw.beginRow(y + row);
|
||||||
|
int colStart, colEnd;
|
||||||
|
pw.bandColRange(x, pxcSlotWidth, colStart, colEnd);
|
||||||
|
for (int col = colStart; col < colEnd; col++) {
|
||||||
|
const int byteIdx = col >> 2; // col / 4
|
||||||
|
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
|
||||||
|
const uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||||
|
pw.writePixel(x + col, pixelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||||
|
int expectedHeight) {
|
||||||
|
// A later pass of the same page render: the payload is already in RAM, skip
|
||||||
|
// the file entirely.
|
||||||
|
const uint64_t cacheHash = imagePathHash(cachePath);
|
||||||
|
if (pxcSlotHash == cacheHash && pxcSlotWidth != 0) {
|
||||||
|
renderRowsFromPxcSlot(renderer, x, y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile cacheFile;
|
||||||
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedWidth, cachedHeight;
|
||||||
|
if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
|
||||||
|
LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||||
|
expectedWidth = cachedWidth;
|
||||||
|
expectedHeight = cachedHeight;
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||||
|
|
||||||
|
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||||
|
|
||||||
|
// First pass of a page render: try to pull the payload into the RAM slot so
|
||||||
|
// the remaining ~12 passes skip SD entirely.
|
||||||
|
if (loadPxcSlot(cacheHash, cacheFile, cachedWidth, cachedHeight, bytesPerRow)) {
|
||||||
|
renderRowsFromPxcSlot(renderer, x, y);
|
||||||
|
LOG_DBG("IMG", "Cache render complete (payload now in RAM)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streaming fallback (slot didn't fit). A failed slot load may have consumed
|
||||||
|
// part of the payload; rewind to just past the header.
|
||||||
|
cacheFile.seek(4);
|
||||||
|
|
||||||
|
// Read several rows per SD access. A one-row-per-read loop here means
|
||||||
|
// cachedHeight (~728) tiny reads through the storage mutex + SdFat; batching
|
||||||
|
// rows into a ~4KB buffer cuts that to ~20 reads per pass without holding the
|
||||||
|
// whole image.
|
||||||
|
int rowsPerRead = 4096 / bytesPerRow;
|
||||||
|
if (rowsPerRead < 1) rowsPerRead = 1;
|
||||||
|
if (rowsPerRead > cachedHeight) rowsPerRead = cachedHeight;
|
||||||
|
uint8_t* readBuffer = (uint8_t*)malloc((size_t)rowsPerRead * bytesPerRow);
|
||||||
|
if (!readBuffer) {
|
||||||
|
// Fall back to a single-row buffer under memory pressure.
|
||||||
|
rowsPerRead = 1;
|
||||||
|
readBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||||
|
}
|
||||||
|
if (!readBuffer) {
|
||||||
|
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectPixelWriter pw;
|
||||||
|
pw.init(renderer);
|
||||||
|
|
||||||
|
int rowsInBuffer = 0;
|
||||||
|
int bufferRow = 0;
|
||||||
|
for (int row = 0; row < cachedHeight; row++) {
|
||||||
|
if (bufferRow >= rowsInBuffer) {
|
||||||
|
const int toRead = (cachedHeight - row < rowsPerRead) ? (cachedHeight - row) : rowsPerRead;
|
||||||
|
const size_t bytes = (size_t)toRead * bytesPerRow;
|
||||||
|
if (cacheFile.read(readBuffer, bytes) != static_cast<int>(bytes)) {
|
||||||
|
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||||
|
free(readBuffer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rowsInBuffer = toRead;
|
||||||
|
bufferRow = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* rowBuffer = readBuffer + (size_t)bufferRow * bytesPerRow;
|
||||||
|
bufferRow++;
|
||||||
|
|
||||||
|
const int destY = y + row;
|
||||||
|
pw.beginRow(destY);
|
||||||
|
// On a grayscale strip pass only a narrow column window of the image is in
|
||||||
|
// the active band; skip the rest instead of unpacking+clipping every pixel.
|
||||||
|
int colStart, colEnd;
|
||||||
|
pw.bandColRange(x, cachedWidth, colStart, colEnd);
|
||||||
|
for (int col = colStart; col < colEnd; col++) {
|
||||||
|
const int byteIdx = col >> 2; // col / 4
|
||||||
|
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
|
||||||
|
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||||
|
|
||||||
|
pw.writePixel(x + col, pixelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(readBuffer);
|
||||||
|
LOG_DBG("IMG", "Cache render complete");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ImageBlock::hasValidCache() const {
|
||||||
|
const auto cachePath = getCachePath(imagePath);
|
||||||
|
HalFile cacheFile;
|
||||||
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedWidth, cachedHeight;
|
||||||
|
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
|
||||||
|
|
||||||
|
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
|
||||||
|
|
||||||
|
void ImageBlock::releaseRenderCache() { releasePxcSlot(); }
|
||||||
|
|
||||||
|
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
|
||||||
|
renderer.fillRect(x, y, width, height, true);
|
||||||
|
if (width > 2 && height > 2) {
|
||||||
|
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||||
|
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||||
|
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||||
|
// suppression, so it would otherwise do a full (discarded) cache render every
|
||||||
|
// page view. Skip it here. The image still draws in the real BW/grayscale
|
||||||
|
// passes; on first view this just moves the one-time decode to the BW pass.
|
||||||
|
FontCacheManager* fcm = renderer.getFontCacheManager();
|
||||||
|
if (fcm && fcm->isScanning()) return;
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||||
|
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Bounds check render position using logical screen dimensions
|
||||||
|
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||||
|
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||||
|
screenHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiled grayscale (#2190): skip the whole image when it doesn't touch the
|
||||||
|
// active band. The per-pixel writer already clips off-band pixels, but without
|
||||||
|
// this each of the ~7 bands per plane re-ran the full cache load / pixel walk
|
||||||
|
// and discarded the result — the dominant cost of AA on image pages. The check
|
||||||
|
// is orientation-aware and returns true when no strip is active, so the BW
|
||||||
|
// pass and non-tiled controllers render the image exactly as before.
|
||||||
|
if (!renderer.glyphIntersectsStrip(x, y, x + width - 1, y + height - 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFailedThisSession(imagePath)) {
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to render from cache first
|
||||||
|
std::string cachePath = getCachePath(imagePath);
|
||||||
|
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||||
|
return; // Successfully rendered from cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// The build only header-probed the image for dimensions; pull the actual
|
||||||
|
// file out of the book now, on first visit to the page.
|
||||||
|
if (!srcPath.empty() && extractFn && !Storage.exists(imagePath.c_str())) {
|
||||||
|
LOG_DBG("IMG", "Lazy-extracting %s -> %s", srcPath.c_str(), imagePath.c_str());
|
||||||
|
if (!extractFn(extractCtx, srcPath.c_str(), imagePath.c_str())) {
|
||||||
|
LOG_ERR("IMG", "Lazy extraction failed: %s", srcPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cache - need to decode the image
|
||||||
|
// Check if image file exists
|
||||||
|
HalFile file;
|
||||||
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||||
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t fileSize = file.size();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (fileSize == 0) {
|
||||||
|
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
RenderConfig config;
|
||||||
|
config.x = x;
|
||||||
|
config.y = y;
|
||||||
|
config.maxWidth = width;
|
||||||
|
config.maxHeight = height;
|
||||||
|
config.useGrayscale = true;
|
||||||
|
config.useDithering = true;
|
||||||
|
config.performanceMode = false;
|
||||||
|
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||||
|
config.cachePath = cachePath; // Enable caching during decode
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||||
|
if (!decoder) {
|
||||||
|
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||||
|
|
||||||
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||||
|
rememberImageFailure(imagePath);
|
||||||
|
renderPlaceholder(renderer, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Decode successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::serialize(HalFile& file) {
|
||||||
|
serialization::writeString(file, imagePath);
|
||||||
|
serialization::writeString(file, srcPath);
|
||||||
|
serialization::writePod(file, width);
|
||||||
|
serialization::writePod(file, height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ImageBlock> ImageBlock::deserialize(HalFile& file) {
|
||||||
|
std::string path;
|
||||||
|
std::string src;
|
||||||
|
serialization::readString(file, path);
|
||||||
|
serialization::readString(file, src);
|
||||||
|
int16_t w, h;
|
||||||
|
serialization::readPod(file, w);
|
||||||
|
serialization::readPod(file, h);
|
||||||
|
return std::unique_ptr<ImageBlock>(new (std::nothrow) ImageBlock(path, src, w, h));
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Block.h"
|
||||||
|
|
||||||
|
class ImageBlock final : public Block {
|
||||||
|
public:
|
||||||
|
ImageBlock(const std::string& imagePath, const std::string& srcPath, int16_t width, int16_t height);
|
||||||
|
~ImageBlock() override = default;
|
||||||
|
|
||||||
|
const std::string& getImagePath() const { return imagePath; }
|
||||||
|
int16_t getWidth() const { return width; }
|
||||||
|
int16_t getHeight() const { return height; }
|
||||||
|
|
||||||
|
bool imageExists() const;
|
||||||
|
bool hasValidCache() const;
|
||||||
|
bool needsDecode() const;
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
|
||||||
|
static void clearSessionRenderFailures();
|
||||||
|
|
||||||
|
// A page render draws its image up to ~13 times (BW double-refresh plus every
|
||||||
|
// grayscale band pass), and each draw streams the whole .pxc off SD. The
|
||||||
|
// first draw caches the pixel payload in RAM (chunked, heap-gated, falls back
|
||||||
|
// to streaming when it doesn't fit); the reader calls this when the page
|
||||||
|
// render completes so nothing stays resident between pages.
|
||||||
|
static void releaseRenderCache();
|
||||||
|
|
||||||
|
// Lazy extraction hook: the section build only header-probes images for their
|
||||||
|
// dimensions; the file at imagePath is extracted out of the book on first
|
||||||
|
// render, via this callback (function pointer + context, not std::function —
|
||||||
|
// this is render-loop code). Registered by the reader activity that owns the
|
||||||
|
// Epub, cleared on its exit.
|
||||||
|
using ExtractFn = bool (*)(void* ctx, const char* srcPath, const char* destPath);
|
||||||
|
static void setExtractor(void* ctx, ExtractFn fn);
|
||||||
|
|
||||||
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
|
bool isEmpty() override { return false; }
|
||||||
|
|
||||||
|
void render(GfxRenderer& renderer, const int x, const int y);
|
||||||
|
bool serialize(HalFile& file);
|
||||||
|
static std::unique_ptr<ImageBlock> deserialize(HalFile& file);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string imagePath;
|
||||||
|
std::string srcPath; // book-internal source href; empty once known-extracted
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
|
||||||
|
static void* extractCtx;
|
||||||
|
static ExtractFn extractFn;
|
||||||
|
};
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
#include "TextBlock.h"
|
||||||
|
|
||||||
|
#include <BidiUtils.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
size_t TextBlock::arenaSize(const uint16_t wordCount, const bool hasFocus, const uint16_t textBytes) {
|
||||||
|
// Layout documented in TextBlock.h: 16-bit arrays first, then 8-bit arrays, then text.
|
||||||
|
size_t size = static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(int16_t) + sizeof(uint8_t));
|
||||||
|
if (hasFocus) {
|
||||||
|
size += static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
return size + textBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::bindArenaPointers() {
|
||||||
|
uint8_t* base = arena.get();
|
||||||
|
const size_t wc = numWords;
|
||||||
|
textOffArr = reinterpret_cast<const uint16_t*>(base);
|
||||||
|
xposArr = reinterpret_cast<const int16_t*>(base + wc * 2);
|
||||||
|
size_t off = wc * 4;
|
||||||
|
if (focusPresent) {
|
||||||
|
focusSuffixXArr = reinterpret_cast<const uint16_t*>(base + off);
|
||||||
|
off += wc * 2;
|
||||||
|
}
|
||||||
|
stylesArr = base + off;
|
||||||
|
off += wc;
|
||||||
|
if (focusPresent) {
|
||||||
|
focusBoundaryArr = base + off;
|
||||||
|
off += wc;
|
||||||
|
}
|
||||||
|
textArr = reinterpret_cast<const char*>(base + off);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextBlock::TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||||
|
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||||
|
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle)
|
||||||
|
: blockStyle(blockStyle) {
|
||||||
|
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
||||||
|
// When present, they must be sized in lockstep with words[].
|
||||||
|
const bool hasFocus = !focusBoundary.empty();
|
||||||
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() || words.size() > 10000 ||
|
||||||
|
(hasFocus && (words.size() != focusBoundary.size() || words.size() != focusSuffixX.size()))) {
|
||||||
|
LOG_ERR("TXB", "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)",
|
||||||
|
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||||
|
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(focusBoundary.size()),
|
||||||
|
static_cast<uint32_t>(focusSuffixX.size()));
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
numWords = static_cast<uint16_t>(words.size());
|
||||||
|
focusPresent = hasFocus;
|
||||||
|
if (numWords == 0) {
|
||||||
|
return; // valid empty block, no arena
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1: total text size, one NUL per word. A line is at most a physical
|
||||||
|
// row of the page, so uint16_t offsets are ample; reject anything larger.
|
||||||
|
size_t totalText = 0;
|
||||||
|
for (const auto& w : words) totalText += w.size() + 1;
|
||||||
|
if (totalText > UINT16_MAX) {
|
||||||
|
LOG_ERR("TXB", "Construction failed: text size %u exceeds arena limit", static_cast<uint32_t>(totalText));
|
||||||
|
numWords = 0;
|
||||||
|
focusPresent = false;
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textBytes = static_cast<uint16_t>(totalText);
|
||||||
|
|
||||||
|
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||||
|
arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||||
|
if (!arena) {
|
||||||
|
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||||
|
numWords = 0;
|
||||||
|
textBytes = 0;
|
||||||
|
focusPresent = false;
|
||||||
|
isValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bindArenaPointers();
|
||||||
|
|
||||||
|
// Pass 2: fill. Mutable aliases of the const views bound above.
|
||||||
|
auto* textOff = const_cast<uint16_t*>(textOffArr);
|
||||||
|
auto* xpos = const_cast<int16_t*>(xposArr);
|
||||||
|
auto* styles = const_cast<uint8_t*>(stylesArr);
|
||||||
|
auto* text = const_cast<char*>(textArr);
|
||||||
|
uint16_t off = 0;
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
textOff[i] = off;
|
||||||
|
xpos[i] = wordXpos[i];
|
||||||
|
styles[i] = static_cast<uint8_t>(wordStyles[i]);
|
||||||
|
memcpy(text + off, words[i].data(), words[i].size());
|
||||||
|
off += static_cast<uint16_t>(words[i].size());
|
||||||
|
text[off++] = '\0';
|
||||||
|
}
|
||||||
|
if (focusPresent) {
|
||||||
|
auto* suffixX = const_cast<uint16_t*>(focusSuffixXArr);
|
||||||
|
auto* boundary = const_cast<uint8_t*>(focusBoundaryArr);
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
suffixX[i] = focusSuffixX[i];
|
||||||
|
boundary[i] = focusBoundary[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
|
if (!isValid) {
|
||||||
|
LOG_ERR("TXB", "Render skipped: invalid block");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool scanning = renderer.isFontCacheScanning();
|
||||||
|
const int ascender = renderer.getFontAscenderSize(fontId);
|
||||||
|
|
||||||
|
struct DecorationLineTracker {
|
||||||
|
EpdFontFamily::Style style;
|
||||||
|
int yOffset;
|
||||||
|
int startX = -1;
|
||||||
|
int endX = -1;
|
||||||
|
int yPos = 0;
|
||||||
|
|
||||||
|
bool active() const { return startX != -1; }
|
||||||
|
void reset() {
|
||||||
|
startX = -1;
|
||||||
|
endX = -1;
|
||||||
|
yPos = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DecorationLineTracker decorationLines[] = {
|
||||||
|
{EpdFontFamily::UNDERLINE, ascender + 2},
|
||||||
|
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto flushDecoration = [&](DecorationLineTracker& line) {
|
||||||
|
if (line.active()) {
|
||||||
|
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
|
||||||
|
line.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto flushDecorations = [&]() {
|
||||||
|
for (auto& line : decorationLines) {
|
||||||
|
flushDecoration(line);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < numWords; i++) {
|
||||||
|
const char* word = wordText(i);
|
||||||
|
const int wordX = xposArr[i] + x;
|
||||||
|
const EpdFontFamily::Style currentStyle = wordStyle(i);
|
||||||
|
const auto baseDir =
|
||||||
|
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
|
||||||
|
const uint8_t boundary = focusBoundary(i);
|
||||||
|
|
||||||
|
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
||||||
|
// drawText, so these offsets are chosen relative to the full-size ascender:
|
||||||
|
// SUP: raise by 40% of ascender — sits clearly above the cap-height
|
||||||
|
// SUB: lower by 25% of ascender — descends below baseline without clashing with ascenders below
|
||||||
|
int wordY = y;
|
||||||
|
if ((currentStyle & EpdFontFamily::SUP) != 0) {
|
||||||
|
wordY -= ascender * 2 / 5;
|
||||||
|
} else if ((currentStyle & EpdFontFamily::SUB) != 0) {
|
||||||
|
wordY += ascender / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundary > 0) {
|
||||||
|
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
|
||||||
|
// The bold prefix is bounded to 9 codepoints by the clamp on targetBoldChars in
|
||||||
|
// ParsedText::addWord; 9 UTF-8 codepoints occupy at most 9 * 4 = 36 bytes, +1 for null = 37.
|
||||||
|
// suffixX is computed at cache-creation time to avoid font metric lookups at render time.
|
||||||
|
static constexpr size_t MAX_FOCUS_PREFIX_BYTES = 9 * 4 + 1;
|
||||||
|
char boldBuf[40];
|
||||||
|
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
||||||
|
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
||||||
|
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
||||||
|
const size_t boldLen =
|
||||||
|
std::min<size_t>({static_cast<size_t>(boundary), static_cast<size_t>(wordTextLen(i)), sizeof(boldBuf) - 1});
|
||||||
|
memcpy(boldBuf, word, boldLen);
|
||||||
|
boldBuf[boldLen] = '\0';
|
||||||
|
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
||||||
|
const int suffixX = wordX + focusSuffixXArr[i];
|
||||||
|
renderer.drawText(fontId, suffixX, wordY, word + boldLen, true, currentStyle, baseDir);
|
||||||
|
} else {
|
||||||
|
renderer.drawText(fontId, wordX, wordY, word, true, currentStyle, baseDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanning) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
|
||||||
|
int lineStartX = wordX;
|
||||||
|
int lineWidth = renderer.getTextWidth(fontId, word, currentStyle, baseDir);
|
||||||
|
|
||||||
|
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||||
|
lineWidth = (lineWidth + 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not decorate the synthetic em-space used for paragraph indentation.
|
||||||
|
if (wordTextLen(i) >= 3 && static_cast<uint8_t>(word[0]) == 0xE2 && static_cast<uint8_t>(word[1]) == 0x80 &&
|
||||||
|
static_cast<uint8_t>(word[2]) == 0x83) {
|
||||||
|
const char* visibleText = word + 3;
|
||||||
|
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||||
|
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
|
||||||
|
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||||
|
lineWidth = (lineWidth + 1) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& line : decorationLines) {
|
||||||
|
if ((currentStyle & line.style) == 0) {
|
||||||
|
flushDecoration(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lineY = wordY + line.yOffset;
|
||||||
|
if (line.active() && line.yPos != lineY) {
|
||||||
|
flushDecoration(line);
|
||||||
|
}
|
||||||
|
if (!line.active()) {
|
||||||
|
line.startX = lineStartX;
|
||||||
|
line.yPos = lineY;
|
||||||
|
}
|
||||||
|
line.endX = lineStartX + lineWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flushDecorations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushDecorations();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextBlock::serialize(HalFile& file) const {
|
||||||
|
if (!isValid) {
|
||||||
|
LOG_ERR("TXB", "Serialization failed: invalid block");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word data: scalars, then the arena verbatim -- its in-memory layout is
|
||||||
|
// exactly the on-disk layout (see TextBlock.h), so one write covers all
|
||||||
|
// per-word arrays and the text blob.
|
||||||
|
serialization::writePod(file, numWords);
|
||||||
|
serialization::writePod(file, static_cast<uint8_t>(focusPresent ? 1 : 0));
|
||||||
|
serialization::writePod(file, textBytes);
|
||||||
|
if (numWords > 0) {
|
||||||
|
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||||
|
if (file.write(arena.get(), size) != size) {
|
||||||
|
LOG_ERR("TXB", "Serialization failed: arena write (%u bytes)", static_cast<uint32_t>(size));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
serialization::writePod(file, blockStyle.isRtl);
|
||||||
|
serialization::writePod(file, blockStyle.directionDefined);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
||||||
|
uint16_t wc;
|
||||||
|
uint8_t hasFocus;
|
||||||
|
uint16_t textBytes;
|
||||||
|
serialization::readPod(file, wc);
|
||||||
|
serialization::readPod(file, hasFocus);
|
||||||
|
serialization::readPod(file, textBytes);
|
||||||
|
|
||||||
|
// Sanity checks: cap the arena allocation and reject impossible geometry
|
||||||
|
// (every word carries at least its NUL terminator).
|
||||||
|
if (wc > 10000) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if ((wc == 0 && textBytes != 0) || (wc > 0 && textBytes < wc)) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: bad text size %u for %u words", textBytes, wc);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<TextBlock> block(new (std::nothrow) TextBlock());
|
||||||
|
if (!block) {
|
||||||
|
LOG_ERR("TXB", "OOM: TextBlock");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
block->numWords = wc;
|
||||||
|
block->textBytes = textBytes;
|
||||||
|
block->focusPresent = hasFocus != 0;
|
||||||
|
|
||||||
|
if (wc > 0) {
|
||||||
|
const size_t size = arenaSize(wc, block->focusPresent, textBytes);
|
||||||
|
block->arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||||
|
if (!block->arena) {
|
||||||
|
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (file.read(block->arena.get(), size) != size) {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: arena read (%u bytes)", static_cast<uint32_t>(size));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
block->bindArenaPointers();
|
||||||
|
|
||||||
|
// Validate offsets before anything dereferences wordText(): offset 0 first,
|
||||||
|
// strictly increasing, in bounds, and every word NUL-terminated (word i ends
|
||||||
|
// at the byte before offset i+1; the last word at the last text byte).
|
||||||
|
const uint16_t* textOff = block->textOffArr;
|
||||||
|
const char* text = block->textArr;
|
||||||
|
if (textOff[0] != 0 || text[textBytes - 1] != '\0') {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: corrupt text layout");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (uint16_t i = 1; i < wc; i++) {
|
||||||
|
if (textOff[i] <= textOff[i - 1] || textOff[i] >= textBytes || text[textOff[i] - 1] != '\0') {
|
||||||
|
LOG_ERR("TXB", "Deserialization failed: corrupt word offset %u", i);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style (alignment + margins/padding/indent)
|
||||||
|
BlockStyle& blockStyle = block->blockStyle;
|
||||||
|
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);
|
||||||
|
serialization::readPod(file, blockStyle.isRtl);
|
||||||
|
serialization::readPod(file, blockStyle.directionDefined);
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Block.h"
|
||||||
|
#include "BlockStyle.h"
|
||||||
|
|
||||||
|
// Represents a line of text on a page.
|
||||||
|
//
|
||||||
|
// All per-word data lives in ONE flat heap allocation (the arena) instead of
|
||||||
|
// six parallel vectors: a resident page holds ~25-30 of these blocks, and the
|
||||||
|
// vector-of-string layout cost ~250 throwing allocations per page load, which
|
||||||
|
// was the primary driver of heap fragmentation on the ESP32-C3.
|
||||||
|
//
|
||||||
|
// Arena layout, in order (2-byte alignment holds by construction: all 16-bit
|
||||||
|
// arrays come first and the arena base is allocator-aligned; RISC-V faults on
|
||||||
|
// unaligned multi-byte access):
|
||||||
|
// uint16_t textOff[wordCount] byte offset of word i's text in text[]
|
||||||
|
// int16_t xpos[wordCount]
|
||||||
|
// uint16_t focusSuffixX[wordCount] present only when focusPresent
|
||||||
|
// uint8_t styles[wordCount]
|
||||||
|
// uint8_t focusBoundary[wordCount] present only when focusPresent
|
||||||
|
// char text[textBytes] all words back to back, NUL-terminated
|
||||||
|
//
|
||||||
|
// Each word is stored NUL-terminated so render() can hand `text + textOff[i]`
|
||||||
|
// straight to C APIs (drawText) with no std::string materialization.
|
||||||
|
//
|
||||||
|
// Focus split semantics (unchanged from the vector layout): boundary N > 0
|
||||||
|
// means the first N bytes of word i render bold, the remainder in the base
|
||||||
|
// style. N is bounded to 9 codepoints (<= 36 UTF-8 bytes) by the clamp in
|
||||||
|
// ParsedText::addWord. focusSuffixX is the pre-computed pixel offset from the
|
||||||
|
// word start to the regular suffix. Both arrays are omitted from the arena
|
||||||
|
// entirely when no word on the line has a split (zero per-word RAM cost when
|
||||||
|
// focus reading is disabled).
|
||||||
|
class TextBlock final : public Block {
|
||||||
|
private:
|
||||||
|
BlockStyle blockStyle;
|
||||||
|
uint16_t numWords = 0;
|
||||||
|
uint16_t textBytes = 0; // total size of the text region, including NULs
|
||||||
|
bool focusPresent = false;
|
||||||
|
bool isValid = true;
|
||||||
|
// The ONLY allocation: makeUniqueNoThrow, so OOM yields an invalid block
|
||||||
|
// instead of abort() (bare new is not nothrow with -fno-exceptions).
|
||||||
|
std::unique_ptr<uint8_t[]> arena;
|
||||||
|
// Typed views into the arena, bound once after the arena is filled. All
|
||||||
|
// 16-bit bases sit at even offsets, so direct dereference is alignment-safe.
|
||||||
|
const uint16_t* textOffArr = nullptr;
|
||||||
|
const int16_t* xposArr = nullptr;
|
||||||
|
const uint16_t* focusSuffixXArr = nullptr; // null when !focusPresent
|
||||||
|
const uint8_t* stylesArr = nullptr;
|
||||||
|
const uint8_t* focusBoundaryArr = nullptr; // null when !focusPresent
|
||||||
|
const char* textArr = nullptr;
|
||||||
|
|
||||||
|
TextBlock() = default; // deserialize() fills the fields directly
|
||||||
|
static size_t arenaSize(uint16_t wordCount, bool hasFocus, uint16_t textBytes);
|
||||||
|
void bindArenaPointers();
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Flatten-on-construct: copies the layout-time vectors into the arena; the
|
||||||
|
// vectors die with the caller. On arena OOM the block is empty and valid()
|
||||||
|
// is false -- callers must check and fail the line instead of using it.
|
||||||
|
explicit TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||||
|
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||||
|
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle = BlockStyle());
|
||||||
|
~TextBlock() override = default;
|
||||||
|
TextBlock(const TextBlock&) = delete;
|
||||||
|
TextBlock& operator=(const TextBlock&) = delete;
|
||||||
|
|
||||||
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||||
|
bool isEmpty() override { return numWords == 0; }
|
||||||
|
bool valid() const { return isValid; }
|
||||||
|
uint16_t wordCount() const { return numWords; }
|
||||||
|
// NUL-terminated by construction; safe to pass to C APIs directly.
|
||||||
|
const char* wordText(const uint16_t i) const { return textArr + textOffArr[i]; }
|
||||||
|
uint16_t wordTextLen(const uint16_t i) const {
|
||||||
|
const uint16_t end = (i + 1 < numWords) ? textOffArr[i + 1] : textBytes;
|
||||||
|
return end - textOffArr[i] - 1; // exclude the NUL
|
||||||
|
}
|
||||||
|
int16_t wordXpos(const uint16_t i) const { return xposArr[i]; }
|
||||||
|
EpdFontFamily::Style wordStyle(const uint16_t i) const { return static_cast<EpdFontFamily::Style>(stylesArr[i]); }
|
||||||
|
uint8_t focusBoundary(const uint16_t i) const { return focusPresent ? focusBoundaryArr[i] : 0; }
|
||||||
|
uint16_t focusSuffixX(const uint16_t i) const { return focusPresent ? focusSuffixXArr[i] : 0; }
|
||||||
|
|
||||||
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
|
bool serialize(HalFile& file) const;
|
||||||
|
static std::unique_ptr<TextBlock> deserialize(HalFile& file);
|
||||||
|
};
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalDisplay.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
// Direct framebuffer writer that eliminates per-pixel overhead from the image
|
||||||
|
// rendering hot path. Pre-computes orientation transform as linear coefficients
|
||||||
|
// and caches render-mode state so the inner loop is: one multiply, one add,
|
||||||
|
// one shift, and one AND per pixel — no branches, no method calls.
|
||||||
|
//
|
||||||
|
// Caller is responsible for ensuring (outX, outY) are within screen bounds.
|
||||||
|
// ImageBlock::render() already validates this before entering the pixel loop,
|
||||||
|
// and the JPEG/PNG callbacks pre-clamp destination ranges to screen bounds.
|
||||||
|
struct DirectPixelWriter {
|
||||||
|
uint8_t* fb;
|
||||||
|
GfxRenderer::RenderMode mode;
|
||||||
|
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
|
||||||
|
// Active write target: for tiled grayscale, fb is the band scratch, originY is
|
||||||
|
// the band's top physical row, and clipRows is the band height. Off-band
|
||||||
|
// pixels are dropped. With no strip active these collapse to the full frame
|
||||||
|
// (originY 0, clipRows panelHeight) so the clip doubles as a bounds guard.
|
||||||
|
int originY;
|
||||||
|
int clipRows;
|
||||||
|
|
||||||
|
// Orientation is collapsed into a linear transform:
|
||||||
|
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
||||||
|
// phyY = phyYBase + x * phyYStepX + y * phyYStepY
|
||||||
|
int phyXBase, phyYBase;
|
||||||
|
int phyXStepX, phyYStepX; // per logical-X step
|
||||||
|
int phyXStepY, phyYStepY; // per logical-Y step
|
||||||
|
|
||||||
|
// Row-precomputed: the Y-dependent portion of the physical coords
|
||||||
|
int rowPhyXBase, rowPhyYBase;
|
||||||
|
|
||||||
|
void init(GfxRenderer& renderer) {
|
||||||
|
fb = renderer.getWriteTarget();
|
||||||
|
originY = renderer.getWriteOriginY();
|
||||||
|
clipRows = renderer.getWriteRows();
|
||||||
|
mode = renderer.getRenderMode();
|
||||||
|
displayWidthBytes = renderer.getDisplayWidthBytes();
|
||||||
|
|
||||||
|
const int phyW = renderer.getDisplayWidth();
|
||||||
|
const int phyH = renderer.getDisplayHeight();
|
||||||
|
|
||||||
|
switch (renderer.getOrientation()) {
|
||||||
|
case GfxRenderer::Portrait:
|
||||||
|
// phyX = y, phyY = (phyH-1) - x
|
||||||
|
phyXBase = 0;
|
||||||
|
phyYBase = phyH - 1;
|
||||||
|
phyXStepX = 0;
|
||||||
|
phyYStepX = -1;
|
||||||
|
phyXStepY = 1;
|
||||||
|
phyYStepY = 0;
|
||||||
|
break;
|
||||||
|
case GfxRenderer::LandscapeClockwise:
|
||||||
|
// phyX = (phyW-1) - x, phyY = (phyH-1) - y
|
||||||
|
phyXBase = phyW - 1;
|
||||||
|
phyYBase = phyH - 1;
|
||||||
|
phyXStepX = -1;
|
||||||
|
phyYStepX = 0;
|
||||||
|
phyXStepY = 0;
|
||||||
|
phyYStepY = -1;
|
||||||
|
break;
|
||||||
|
case GfxRenderer::PortraitInverted:
|
||||||
|
// phyX = (phyW-1) - y, phyY = x
|
||||||
|
phyXBase = phyW - 1;
|
||||||
|
phyYBase = 0;
|
||||||
|
phyXStepX = 0;
|
||||||
|
phyYStepX = 1;
|
||||||
|
phyXStepY = -1;
|
||||||
|
phyYStepY = 0;
|
||||||
|
break;
|
||||||
|
case GfxRenderer::LandscapeCounterClockwise:
|
||||||
|
// phyX = x, phyY = y
|
||||||
|
phyXBase = 0;
|
||||||
|
phyYBase = 0;
|
||||||
|
phyXStepX = 1;
|
||||||
|
phyYStepX = 0;
|
||||||
|
phyXStepY = 0;
|
||||||
|
phyYStepY = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Fallback to LandscapeCounterClockwise (identity transform)
|
||||||
|
phyXBase = 0;
|
||||||
|
phyYBase = 0;
|
||||||
|
phyXStepX = 1;
|
||||||
|
phyYStepX = 0;
|
||||||
|
phyXStepY = 0;
|
||||||
|
phyYStepY = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call once per row before the column loop.
|
||||||
|
// Pre-computes the Y-dependent portion so writePixel() only needs the X part.
|
||||||
|
inline void beginRow(int logicalY) {
|
||||||
|
rowPhyXBase = phyXBase + logicalY * phyXStepY;
|
||||||
|
rowPhyYBase = phyYBase + logicalY * phyYStepY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the current row (set via beginRow), narrow [colStart, colEnd) to the
|
||||||
|
// columns whose pixels fall inside the active strip band. writePixel() would
|
||||||
|
// clip the rest anyway, but on a strip pass that is most of a full-page image
|
||||||
|
// (only ~one strip-height worth of columns survive in portrait); skipping them
|
||||||
|
// here avoids the per-pixel unpack+transform entirely. For full-frame passes
|
||||||
|
// (clipRows == panel height) the range is unchanged. xBase is the logical X of
|
||||||
|
// column 0; the band test mirrors writePixel(): 0 <= phyY - originY < clipRows.
|
||||||
|
inline void bandColRange(int xBase, int width, int& colStart, int& colEnd) const {
|
||||||
|
// init() only ever sets phyYStepX to 0, +1, or -1; the +1/-1 solve below
|
||||||
|
// relies on that.
|
||||||
|
assert(phyYStepX == 0 || phyYStepX == 1 || phyYStepX == -1);
|
||||||
|
colStart = 0;
|
||||||
|
colEnd = width;
|
||||||
|
if (phyYStepX == 0) {
|
||||||
|
// phyY is constant across the row: the whole row is in-band or out.
|
||||||
|
const int sy = rowPhyYBase - originY;
|
||||||
|
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) colEnd = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// phyY = rowPhyYBase + logicalX * phyYStepX (phyYStepX is +1 or -1).
|
||||||
|
// Solve originY <= phyY <= originY + clipRows - 1 for logicalX.
|
||||||
|
const int loY = originY;
|
||||||
|
const int hiY = originY + clipRows - 1;
|
||||||
|
int xLo, xHi;
|
||||||
|
if (phyYStepX > 0) {
|
||||||
|
xLo = loY - rowPhyYBase;
|
||||||
|
xHi = hiY - rowPhyYBase;
|
||||||
|
} else {
|
||||||
|
xLo = rowPhyYBase - hiY;
|
||||||
|
xHi = rowPhyYBase - loY;
|
||||||
|
}
|
||||||
|
const int cs = xLo - xBase;
|
||||||
|
const int ce = xHi - xBase + 1; // exclusive
|
||||||
|
if (cs > colStart) colStart = cs;
|
||||||
|
if (ce < colEnd) colEnd = ce;
|
||||||
|
if (colStart < 0) colStart = 0;
|
||||||
|
if (colEnd > width) colEnd = width;
|
||||||
|
if (colStart > colEnd) colStart = colEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a single 2-bit dithered pixel value to the framebuffer.
|
||||||
|
// Must be called after beginRow() for the current row.
|
||||||
|
// No bounds checking — caller guarantees coordinates are valid.
|
||||||
|
inline void writePixel(int logicalX, uint8_t pixelValue) const {
|
||||||
|
// Determine whether to draw based on render mode
|
||||||
|
bool draw;
|
||||||
|
bool state;
|
||||||
|
switch (mode) {
|
||||||
|
case GfxRenderer::BW:
|
||||||
|
draw = (pixelValue < 3);
|
||||||
|
state = true;
|
||||||
|
break;
|
||||||
|
case GfxRenderer::GRAYSCALE_MSB:
|
||||||
|
draw = (pixelValue == 1 || pixelValue == 2);
|
||||||
|
state = false;
|
||||||
|
break;
|
||||||
|
case GfxRenderer::GRAYSCALE_LSB:
|
||||||
|
draw = (pixelValue == 1);
|
||||||
|
state = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!draw) return;
|
||||||
|
|
||||||
|
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
||||||
|
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
||||||
|
|
||||||
|
// Band-local row. The unsigned compare drops both off-band pixels (strip
|
||||||
|
// mode) and any out-of-frame row (full-frame mode) in one branch.
|
||||||
|
const int sy = phyY - originY;
|
||||||
|
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) return;
|
||||||
|
|
||||||
|
const uint16_t byteIndex = static_cast<uint16_t>(sy * displayWidthBytes + (phyX >> 3));
|
||||||
|
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
fb[byteIndex] &= ~bitMask; // Clear bit (draw black)
|
||||||
|
} else {
|
||||||
|
fb[byteIndex] |= bitMask; // Set bit (draw white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direct cache writer that eliminates per-pixel overhead from PixelCache::setPixel().
|
||||||
|
// Pre-computes row pointer so the inner loop is just byte index + bit manipulation.
|
||||||
|
//
|
||||||
|
// The cache buffer is a small streaming band (e.g. 16 rows), not the full image,
|
||||||
|
// so a band-relative row/column that lands outside it would corrupt adjacent
|
||||||
|
// heap. This writer therefore bounds-checks every access: beginRow() invalidates
|
||||||
|
// the row when it falls outside the band, and writePixel() drops out-of-range
|
||||||
|
// columns. This path only runs during the single decode that populates the
|
||||||
|
// cache, never on the screen render hot path, so the checks are cheap.
|
||||||
|
struct DirectCacheWriter {
|
||||||
|
uint8_t* buffer;
|
||||||
|
int bytesPerRow;
|
||||||
|
int bandRows;
|
||||||
|
int originX;
|
||||||
|
uint8_t* rowPtr; // Pre-computed for current row; nullptr if row is out of band
|
||||||
|
|
||||||
|
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheBandRows, int cacheOriginX) {
|
||||||
|
buffer = cacheBuffer;
|
||||||
|
bytesPerRow = cacheBytesPerRow;
|
||||||
|
bandRows = cacheBandRows;
|
||||||
|
originX = cacheOriginX;
|
||||||
|
rowPtr = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call once per row before the column loop. Drops rows outside the band.
|
||||||
|
inline void beginRow(int screenY, int cacheOriginY) {
|
||||||
|
const int localRow = screenY - cacheOriginY;
|
||||||
|
rowPtr = (static_cast<unsigned>(localRow) < static_cast<unsigned>(bandRows))
|
||||||
|
? buffer + (size_t)localRow * bytesPerRow
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a 2-bit pixel value. Drops the write if the row is out of band or the
|
||||||
|
// column is out of range.
|
||||||
|
inline void writePixel(int screenX, uint8_t value) const {
|
||||||
|
if (!rowPtr) return;
|
||||||
|
const int localX = screenX - originX;
|
||||||
|
const int byteIdx = localX >> 2; // localX / 4
|
||||||
|
if (static_cast<unsigned>(byteIdx) >= static_cast<unsigned>(bytesPerRow)) return;
|
||||||
|
const int bitShift = 6 - (localX & 3) * 2; // MSB first: pixel 0 at bits 6-7
|
||||||
|
rowPtr[byteIdx] = (rowPtr[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// 4x4 Bayer matrix for ordered dithering
|
||||||
|
inline const uint8_t bayer4x4[4][4] = {
|
||||||
|
{0, 8, 2, 10},
|
||||||
|
{12, 4, 14, 6},
|
||||||
|
{3, 11, 1, 9},
|
||||||
|
{15, 7, 13, 5},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply Bayer dithering and quantize to 4 levels (0-3)
|
||||||
|
// Stateless - works correctly with any pixel processing order
|
||||||
|
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||||
|
int bayer = bayer4x4[y & 3][x & 3];
|
||||||
|
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
|
||||||
|
|
||||||
|
int adjusted = gray + dither;
|
||||||
|
if (adjusted < 0) adjusted = 0;
|
||||||
|
if (adjusted > 255) adjusted = 255;
|
||||||
|
|
||||||
|
if (adjusted < 64) return 0;
|
||||||
|
if (adjusted < 128) return 1;
|
||||||
|
if (adjusted < 192) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include "ImageDecoderFactory.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "JpegToFramebufferConverter.h"
|
||||||
|
#include "PngToFramebufferConverter.h"
|
||||||
|
|
||||||
|
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||||
|
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||||
|
std::string ext = imagePath;
|
||||||
|
size_t dotPos = ext.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
ext = ext.substr(dotPos);
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ext = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||||
|
if (!jpegDecoder) {
|
||||||
|
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||||
|
}
|
||||||
|
return jpegDecoder.get();
|
||||||
|
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||||
|
if (!pngDecoder) {
|
||||||
|
pngDecoder.reset(new PngToFramebufferConverter());
|
||||||
|
}
|
||||||
|
return pngDecoder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class JpegToFramebufferConverter;
|
||||||
|
class PngToFramebufferConverter;
|
||||||
|
|
||||||
|
class ImageDecoderFactory {
|
||||||
|
public:
|
||||||
|
// Returns non-owning pointer - factory owns the decoder lifetime
|
||||||
|
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
|
||||||
|
static bool isFormatSupported(const std::string& imagePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
|
||||||
|
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
|
||||||
|
};
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#include "ImageDimsProbe.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// SOFn markers carry the frame dimensions. C4 (DHT), C8 (JPG extension) and
|
||||||
|
// CC (DAC) share the 0xCn range but are not frame headers.
|
||||||
|
bool isJpegSof(const uint8_t marker) {
|
||||||
|
return marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC;
|
||||||
|
}
|
||||||
|
constexpr uint8_t PNG_SIG[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ImageDimsProbe::feed(const uint8_t b) {
|
||||||
|
switch (state) {
|
||||||
|
case State::Sniff:
|
||||||
|
if (b == 0xFF) {
|
||||||
|
state = State::JpegSoi;
|
||||||
|
} else if (b == PNG_SIG[0]) {
|
||||||
|
state = State::PngHeader;
|
||||||
|
} else {
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pos = 1;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::PngHeader:
|
||||||
|
// Bytes 1..7: signature; 8..11: IHDR length; 12..15: "IHDR"; 16..23: dims.
|
||||||
|
if (pos < 8) {
|
||||||
|
if (b != PNG_SIG[pos]) {
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (pos >= 12 && pos < 16) {
|
||||||
|
if (b != "IHDR"[pos - 12]) {
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (pos >= 16 && pos < 20) {
|
||||||
|
width = static_cast<uint16_t>((static_cast<uint32_t>(width) << 8) | b);
|
||||||
|
} else if (pos >= 20 && pos < 24) {
|
||||||
|
height = static_cast<uint16_t>((static_cast<uint32_t>(height) << 8) | b);
|
||||||
|
if (pos == 23) {
|
||||||
|
state = State::Done;
|
||||||
|
pos++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegSoi:
|
||||||
|
if (b != 0xD8) {
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state = State::JpegFf;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegFf:
|
||||||
|
if (b != 0xFF) {
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state = State::JpegMarker;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegMarker:
|
||||||
|
if (b == 0xFF) return true; // fill bytes before a marker are legal
|
||||||
|
if (b == 0x01 || (b >= 0xD0 && b <= 0xD8)) {
|
||||||
|
// TEM / RSTn / SOI: standalone, no length field.
|
||||||
|
state = State::JpegFf;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (b == 0xD9 || b == 0xDA) {
|
||||||
|
// EOI or SOS before any SOF: no dimensions to be found.
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sofPending = isJpegSof(b);
|
||||||
|
state = State::JpegLenHi;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegLenHi:
|
||||||
|
segLen = static_cast<uint16_t>(b << 8);
|
||||||
|
state = State::JpegLenLo;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegLenLo:
|
||||||
|
segLen = static_cast<uint16_t>(segLen | b);
|
||||||
|
if (segLen < 2 || (sofPending && segLen < 7)) {
|
||||||
|
state = State::Failed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sofPending) {
|
||||||
|
sofFill = 0;
|
||||||
|
state = State::JpegSof;
|
||||||
|
} else if (segLen == 2) {
|
||||||
|
state = State::JpegFf;
|
||||||
|
} else {
|
||||||
|
skipLeft = static_cast<uint32_t>(segLen) - 2;
|
||||||
|
state = State::JpegSkip;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegSkip:
|
||||||
|
if (--skipLeft == 0) state = State::JpegFf;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::JpegSof:
|
||||||
|
sofBuf[sofFill++] = b;
|
||||||
|
if (sofFill == 5) {
|
||||||
|
height = static_cast<uint16_t>((sofBuf[1] << 8) | sofBuf[2]);
|
||||||
|
width = static_cast<uint16_t>((sofBuf[3] << 8) | sofBuf[4]);
|
||||||
|
state = State::Done;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::Done:
|
||||||
|
case State::Failed:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ImageDimsProbe::write(const uint8_t b) { return feed(b) ? 1 : 0; }
|
||||||
|
|
||||||
|
size_t ImageDimsProbe::write(const uint8_t* data, const size_t len) {
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (!feed(data[i])) return i; // short write: polite early stop
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageDimsProbe::getDimensions(ImageDimensions& out) const {
|
||||||
|
if (state != State::Done || width == 0 || height == 0 || width > INT16_MAX || height > INT16_MAX) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.width = static_cast<int16_t>(width);
|
||||||
|
out.height = static_cast<int16_t>(height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Print.h>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
// Streaming JPEG/PNG header parser: finds image dimensions from the first few
|
||||||
|
// KB of a compressed stream without inflating the whole image. Feed bytes via
|
||||||
|
// the Print interface (e.g. Epub::readItemContentsToStream with
|
||||||
|
// allowEarlyStop=true); write() returns short once the dimensions are known or
|
||||||
|
// the stream is known to be unusable, which the zip layer treats as a polite
|
||||||
|
// early stop rather than an error.
|
||||||
|
//
|
||||||
|
// JPEG: walks marker segments (skipping EXIF/APPn of any size statefully, so
|
||||||
|
// nothing is buffered) until a SOFn frame header yields the dimensions.
|
||||||
|
// PNG: reads the IHDR fields at their fixed offsets (bytes 16..23).
|
||||||
|
class ImageDimsProbe : public Print {
|
||||||
|
public:
|
||||||
|
size_t write(uint8_t b) override;
|
||||||
|
size_t write(const uint8_t* data, size_t len) override;
|
||||||
|
|
||||||
|
// True only when a valid header was found; fills `out`.
|
||||||
|
bool getDimensions(ImageDimensions& out) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool feed(uint8_t b); // returns false once parsing is finished (found or failed)
|
||||||
|
|
||||||
|
enum class State : uint8_t {
|
||||||
|
Sniff, // first byte decides the format
|
||||||
|
PngHeader, // PNG signature + IHDR at fixed offsets
|
||||||
|
JpegSoi, // second SOI byte (0xD8)
|
||||||
|
JpegFf, // expect a 0xFF marker prefix
|
||||||
|
JpegMarker, // marker type byte (0xFF padding allowed)
|
||||||
|
JpegLenHi, // segment length, high byte
|
||||||
|
JpegLenLo, // segment length, low byte
|
||||||
|
JpegSkip, // skipping a non-SOF segment body
|
||||||
|
JpegSof, // collecting the 5 SOF bytes: precision, height(2), width(2)
|
||||||
|
Done,
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
State state = State::Sniff;
|
||||||
|
uint32_t pos = 0; // absolute stream offset (PNG fixed-offset parsing)
|
||||||
|
uint32_t skipLeft = 0; // remaining segment bytes to skip
|
||||||
|
uint16_t segLen = 0;
|
||||||
|
bool sofPending = false; // current segment is a SOF frame header
|
||||||
|
uint8_t sofBuf[5] = {0};
|
||||||
|
uint8_t sofFill = 0;
|
||||||
|
uint16_t width = 0;
|
||||||
|
uint16_t height = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||||
|
if (width * height > MAX_SOURCE_PIXELS) {
|
||||||
|
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
||||||
|
format.c_str(), MAX_SOURCE_PIXELS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||||
|
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||||
|
imagePath.c_str());
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
struct ImageDimensions {
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderConfig {
|
||||||
|
int x, y;
|
||||||
|
int maxWidth, maxHeight;
|
||||||
|
bool useGrayscale = true;
|
||||||
|
bool useDithering = true;
|
||||||
|
bool performanceMode = false;
|
||||||
|
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
|
||||||
|
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
virtual ~ImageToFramebufferDecoder() = default;
|
||||||
|
|
||||||
|
virtual bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) = 0;
|
||||||
|
|
||||||
|
virtual bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const = 0;
|
||||||
|
|
||||||
|
virtual const char* getFormatName() const = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Size validation helpers
|
||||||
|
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
||||||
|
|
||||||
|
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||||
|
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||||
|
};
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
#include "JpegToFramebufferConverter.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <JPEGDEC.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
#include "DirectPixelWriter.h"
|
||||||
|
#include "DitherUtils.h"
|
||||||
|
#include "PixelCache.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Context struct passed through JPEGDEC callbacks to avoid global mutable state.
|
||||||
|
// The draw callback receives this via pDraw->pUser (set by setUserPointer()).
|
||||||
|
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by jpegOpen()).
|
||||||
|
struct JpegContext {
|
||||||
|
GfxRenderer* renderer{nullptr};
|
||||||
|
const RenderConfig* config{nullptr};
|
||||||
|
int screenWidth{0};
|
||||||
|
int screenHeight{0};
|
||||||
|
|
||||||
|
// Source dimensions after JPEGDEC's built-in scaling
|
||||||
|
int scaledSrcWidth{0};
|
||||||
|
int scaledSrcHeight{0};
|
||||||
|
|
||||||
|
// Final output dimensions
|
||||||
|
int dstWidth{0};
|
||||||
|
int dstHeight{0};
|
||||||
|
|
||||||
|
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU).
|
||||||
|
// X and Y axes use separate scale factors: the aspect ratio of the output (dstWidth/dstHeight)
|
||||||
|
// may differ from the source (srcWidth/srcHeight) due to integer rounding of displayHeight.
|
||||||
|
// Using a single (X-based) scale for both axes causes the wrong srcRow to be skipped
|
||||||
|
// during nearest-neighbor downscaling, potentially losing critical image content.
|
||||||
|
int32_t fineScaleFPX{1 << 16}; // X: src -> dst column mapping
|
||||||
|
int32_t invScaleFPX{1 << 16}; // X: dst -> src column mapping
|
||||||
|
int32_t fineScaleFPY{1 << 16}; // Y: src -> dst row mapping
|
||||||
|
int32_t invScaleFPY{1 << 16}; // Y: dst -> src row mapping
|
||||||
|
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||||
|
// avoiding the need for global file state.
|
||||||
|
void* jpegOpen(const char* filename, int32_t* size) {
|
||||||
|
HalFile* f = new HalFile();
|
||||||
|
if (!Storage.openFileForRead("JPG", std::string(filename), *f)) {
|
||||||
|
delete f;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
*size = f->size();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void jpegClose(void* handle) {
|
||||||
|
HalFile* f = reinterpret_cast<HalFile*>(handle);
|
||||||
|
if (f) {
|
||||||
|
f->close();
|
||||||
|
delete f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JPEGDEC tracks file position via pFile->iPos internally (e.g. JPEGGetMoreData
|
||||||
|
// checks iPos < iSize to decide whether more data is available). The callbacks
|
||||||
|
// MUST maintain iPos to match the actual file position, otherwise progressive
|
||||||
|
// JPEGs with large headers fail during parsing.
|
||||||
|
int32_t jpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||||
|
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||||
|
if (!f) return 0;
|
||||||
|
int32_t bytesRead = f->read(pBuf, len);
|
||||||
|
if (bytesRead < 0) return 0;
|
||||||
|
pFile->iPos += bytesRead;
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
|
||||||
|
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||||
|
if (!f) return -1;
|
||||||
|
if (!f->seek(pos)) return -1;
|
||||||
|
pFile->iPos = pos;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JPEGDEC object is ~17 KB due to internal decode buffers.
|
||||||
|
// Heap-allocate on demand so memory is only used during active decode.
|
||||||
|
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
|
||||||
|
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
|
||||||
|
|
||||||
|
// Choose JPEGDEC's built-in scale factor for coarse downscaling.
|
||||||
|
// Returns the scale denominator (1, 2, 4, or 8) and sets jpegScaleOption.
|
||||||
|
int chooseJpegScale(float targetScale, int& jpegScaleOption) {
|
||||||
|
if (targetScale <= 0.125f) {
|
||||||
|
jpegScaleOption = JPEG_SCALE_EIGHTH;
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if (targetScale <= 0.25f) {
|
||||||
|
jpegScaleOption = JPEG_SCALE_QUARTER;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (targetScale <= 0.5f) {
|
||||||
|
jpegScaleOption = JPEG_SCALE_HALF;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
jpegScaleOption = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed-point 16.16 arithmetic avoids software float emulation on ESP32-C3 (no FPU).
|
||||||
|
constexpr int FP_SHIFT = 16;
|
||||||
|
constexpr int32_t FP_ONE = 1 << FP_SHIFT;
|
||||||
|
constexpr int32_t FP_MASK = FP_ONE - 1;
|
||||||
|
|
||||||
|
int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||||
|
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
|
||||||
|
if (!ctx || !ctx->config || !ctx->renderer) return 0;
|
||||||
|
|
||||||
|
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
|
||||||
|
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
|
||||||
|
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
|
||||||
|
const int stride = pDraw->iWidth;
|
||||||
|
const int validW = pDraw->iWidthUsed;
|
||||||
|
const int blockH = pDraw->iHeight;
|
||||||
|
|
||||||
|
if (stride <= 0 || blockH <= 0 || validW <= 0) return 1;
|
||||||
|
|
||||||
|
const bool useDithering = ctx->config->useDithering;
|
||||||
|
bool caching = ctx->caching;
|
||||||
|
const int32_t fineScaleFPX = ctx->fineScaleFPX;
|
||||||
|
const int32_t invScaleFPX = ctx->invScaleFPX;
|
||||||
|
const int32_t fineScaleFPY = ctx->fineScaleFPY;
|
||||||
|
const int32_t invScaleFPY = ctx->invScaleFPY;
|
||||||
|
GfxRenderer& renderer = *ctx->renderer;
|
||||||
|
const int cfgX = ctx->config->x;
|
||||||
|
const int cfgY = ctx->config->y;
|
||||||
|
const int blockX = pDraw->x;
|
||||||
|
const int blockY = pDraw->y;
|
||||||
|
|
||||||
|
// Determine destination pixel range covered by this source block
|
||||||
|
const int srcYEnd = blockY + blockH;
|
||||||
|
const int srcXEnd = blockX + validW;
|
||||||
|
|
||||||
|
int dstYStart = (int)((int64_t)blockY * fineScaleFPY >> FP_SHIFT);
|
||||||
|
int dstYEnd = (srcYEnd >= ctx->scaledSrcHeight) ? ctx->dstHeight : (int)((int64_t)srcYEnd * fineScaleFPY >> FP_SHIFT);
|
||||||
|
int dstXStart = (int)((int64_t)blockX * fineScaleFPX >> FP_SHIFT);
|
||||||
|
int dstXEnd = (srcXEnd >= ctx->scaledSrcWidth) ? ctx->dstWidth : (int)((int64_t)srcXEnd * fineScaleFPX >> FP_SHIFT);
|
||||||
|
|
||||||
|
// Pre-clamp destination ranges to screen bounds (eliminates per-pixel screen checks)
|
||||||
|
int clampYMax = ctx->dstHeight;
|
||||||
|
if (ctx->screenHeight - cfgY < clampYMax) clampYMax = ctx->screenHeight - cfgY;
|
||||||
|
if (dstYStart < -cfgY) dstYStart = -cfgY;
|
||||||
|
if (dstYEnd > clampYMax) dstYEnd = clampYMax;
|
||||||
|
|
||||||
|
int clampXMax = ctx->dstWidth;
|
||||||
|
if (ctx->screenWidth - cfgX < clampXMax) clampXMax = ctx->screenWidth - cfgX;
|
||||||
|
if (dstXStart < -cfgX) dstXStart = -cfgX;
|
||||||
|
if (dstXEnd > clampXMax) dstXEnd = clampXMax;
|
||||||
|
|
||||||
|
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
|
||||||
|
|
||||||
|
// Pre-compute orientation and render-mode state once per callback invocation
|
||||||
|
DirectPixelWriter pw;
|
||||||
|
pw.init(renderer);
|
||||||
|
|
||||||
|
// The cache streams to disk one MCU-row band at a time. Flushing rows below
|
||||||
|
// this block (raster order guarantees they are final) repositions the band;
|
||||||
|
// cacheOriginY then maps screen rows to the band-local buffer rows. If a flush
|
||||||
|
// write fails, stop caching for the rest of this decode (and let finalize drop
|
||||||
|
// the partial file) rather than writing past the band buffer.
|
||||||
|
DirectCacheWriter cw;
|
||||||
|
int cacheOriginY = 0;
|
||||||
|
if (caching) {
|
||||||
|
if (!ctx->cache.advanceTo(dstYStart)) {
|
||||||
|
caching = false;
|
||||||
|
ctx->caching = false;
|
||||||
|
} else {
|
||||||
|
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||||
|
cacheOriginY = ctx->config->y + ctx->cache.bandStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 1:1 fast path: no scaling math ===
|
||||||
|
if (fineScaleFPX == FP_ONE && fineScaleFPY == FP_ONE) {
|
||||||
|
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||||
|
const int outY = cfgY + dstY;
|
||||||
|
pw.beginRow(outY);
|
||||||
|
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||||
|
const uint8_t* row = &pixels[(dstY - blockY) * stride];
|
||||||
|
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
|
||||||
|
const int outX = cfgX + dstX;
|
||||||
|
uint8_t gray = row[dstX - blockX];
|
||||||
|
uint8_t dithered;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, dithered);
|
||||||
|
if (caching) cw.writePixel(outX, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Bilinear interpolation (upscale: fineScale > 1.0) ===
|
||||||
|
// Smooths block boundaries that would otherwise create visible banding
|
||||||
|
// on progressive JPEG DC-only decode (1/8 resolution upscaled to target).
|
||||||
|
if (fineScaleFPX > FP_ONE && fineScaleFPY > FP_ONE) {
|
||||||
|
// Pre-compute safe X range where lx0 and lx0+1 are both in [0, validW-1].
|
||||||
|
// Only the left/right edge pixels (typically 0-2 and 1-8 respectively) need clamping.
|
||||||
|
int safeXStart = (int)(((int64_t)blockX * fineScaleFPX + FP_MASK) >> FP_SHIFT);
|
||||||
|
int safeXEnd = (int)((int64_t)(blockX + validW - 1) * fineScaleFPX >> FP_SHIFT);
|
||||||
|
if (safeXStart < dstXStart) safeXStart = dstXStart;
|
||||||
|
if (safeXEnd > dstXEnd) safeXEnd = dstXEnd;
|
||||||
|
if (safeXStart > safeXEnd) safeXEnd = safeXStart;
|
||||||
|
|
||||||
|
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||||
|
const int outY = cfgY + dstY;
|
||||||
|
pw.beginRow(outY);
|
||||||
|
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||||
|
const int32_t srcFyFP = dstY * invScaleFPY;
|
||||||
|
const int32_t fy = srcFyFP & FP_MASK;
|
||||||
|
const int32_t fyInv = FP_ONE - fy;
|
||||||
|
int ly0 = (srcFyFP >> FP_SHIFT) - blockY;
|
||||||
|
int ly1 = ly0 + 1;
|
||||||
|
if (ly0 < 0) ly0 = 0;
|
||||||
|
if (ly0 >= blockH) ly0 = blockH - 1;
|
||||||
|
if (ly1 >= blockH) ly1 = blockH - 1;
|
||||||
|
|
||||||
|
const uint8_t* row0 = &pixels[ly0 * stride];
|
||||||
|
const uint8_t* row1 = &pixels[ly1 * stride];
|
||||||
|
|
||||||
|
// Left edge (with X boundary clamping)
|
||||||
|
for (int dstX = dstXStart; dstX < safeXStart; dstX++) {
|
||||||
|
const int outX = cfgX + dstX;
|
||||||
|
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||||
|
const int32_t fx = srcFxFP & FP_MASK;
|
||||||
|
const int32_t fxInv = FP_ONE - fx;
|
||||||
|
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
|
||||||
|
int lx1 = lx0 + 1;
|
||||||
|
if (lx0 < 0) lx0 = 0;
|
||||||
|
if (lx1 < 0) lx1 = 0;
|
||||||
|
if (lx0 >= validW) lx0 = validW - 1;
|
||||||
|
if (lx1 >= validW) lx1 = validW - 1;
|
||||||
|
|
||||||
|
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
|
||||||
|
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
|
||||||
|
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
|
||||||
|
|
||||||
|
uint8_t dithered;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, dithered);
|
||||||
|
if (caching) cw.writePixel(outX, dithered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
|
||||||
|
for (int dstX = safeXStart; dstX < safeXEnd; dstX++) {
|
||||||
|
const int outX = cfgX + dstX;
|
||||||
|
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||||
|
const int32_t fx = srcFxFP & FP_MASK;
|
||||||
|
const int32_t fxInv = FP_ONE - fx;
|
||||||
|
const int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
|
||||||
|
|
||||||
|
int top = ((int)row0[lx0] * fxInv + (int)row0[lx0 + 1] * fx) >> FP_SHIFT;
|
||||||
|
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx0 + 1] * fx) >> FP_SHIFT;
|
||||||
|
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
|
||||||
|
|
||||||
|
uint8_t dithered;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, dithered);
|
||||||
|
if (caching) cw.writePixel(outX, dithered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right edge (with X boundary clamping)
|
||||||
|
for (int dstX = safeXEnd; dstX < dstXEnd; dstX++) {
|
||||||
|
const int outX = cfgX + dstX;
|
||||||
|
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||||
|
const int32_t fx = srcFxFP & FP_MASK;
|
||||||
|
const int32_t fxInv = FP_ONE - fx;
|
||||||
|
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
|
||||||
|
int lx1 = lx0 + 1;
|
||||||
|
if (lx0 >= validW) lx0 = validW - 1;
|
||||||
|
if (lx1 >= validW) lx1 = validW - 1;
|
||||||
|
|
||||||
|
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
|
||||||
|
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
|
||||||
|
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
|
||||||
|
|
||||||
|
uint8_t dithered;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, dithered);
|
||||||
|
if (caching) cw.writePixel(outX, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
|
||||||
|
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||||
|
const int outY = cfgY + dstY;
|
||||||
|
pw.beginRow(outY);
|
||||||
|
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||||
|
const int32_t srcFyFP = dstY * invScaleFPY;
|
||||||
|
int ly = (srcFyFP >> FP_SHIFT) - blockY;
|
||||||
|
if (ly < 0) ly = 0;
|
||||||
|
if (ly >= blockH) ly = blockH - 1;
|
||||||
|
const uint8_t* row = &pixels[ly * stride];
|
||||||
|
|
||||||
|
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
|
||||||
|
const int outX = cfgX + dstX;
|
||||||
|
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||||
|
int lx = (srcFxFP >> FP_SHIFT) - blockX;
|
||||||
|
if (lx < 0) lx = 0;
|
||||||
|
if (lx >= validW) lx = validW - 1;
|
||||||
|
uint8_t gray = row[lx];
|
||||||
|
|
||||||
|
uint8_t dithered;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, dithered);
|
||||||
|
if (caching) cw.writePixel(outX, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
|
||||||
|
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
|
||||||
|
if (!jpeg) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate JPEG decoder for dimensions");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, nullptr);
|
||||||
|
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
|
||||||
|
if (rc != 1) {
|
||||||
|
LOG_ERR("JPG", "Failed to open JPEG for dimensions (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = jpeg->getWidth();
|
||||||
|
out.height = jpeg->getHeight();
|
||||||
|
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||||
|
const RenderConfig& config) {
|
||||||
|
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
|
||||||
|
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
|
||||||
|
if (!jpeg) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JpegContext ctx;
|
||||||
|
ctx.renderer = &renderer;
|
||||||
|
ctx.config = &config;
|
||||||
|
ctx.screenWidth = renderer.getScreenWidth();
|
||||||
|
ctx.screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
|
||||||
|
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
|
||||||
|
if (rc != 1) {
|
||||||
|
LOG_ERR("JPG", "Failed to open JPEG (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int srcWidth = jpeg->getWidth();
|
||||||
|
int srcHeight = jpeg->getHeight();
|
||||||
|
|
||||||
|
if (srcWidth <= 0 || srcHeight <= 0) {
|
||||||
|
LOG_ERR("JPG", "Invalid JPEG dimensions: %dx%d", srcWidth, srcHeight);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isProgressive = jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE;
|
||||||
|
if (isProgressive) {
|
||||||
|
LOG_INF("JPG", "Progressive JPEG detected - decoding DC coefficients only (lower quality)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate overall target scale
|
||||||
|
float targetScale;
|
||||||
|
int destWidth, destHeight;
|
||||||
|
|
||||||
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
|
destWidth = config.maxWidth;
|
||||||
|
destHeight = config.maxHeight;
|
||||||
|
targetScale = (float)destWidth / srcWidth;
|
||||||
|
} else {
|
||||||
|
float scaleX = (config.maxWidth > 0 && srcWidth > config.maxWidth) ? (float)config.maxWidth / srcWidth : 1.0f;
|
||||||
|
float scaleY = (config.maxHeight > 0 && srcHeight > config.maxHeight) ? (float)config.maxHeight / srcHeight : 1.0f;
|
||||||
|
targetScale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (targetScale > 1.0f) targetScale = 1.0f;
|
||||||
|
|
||||||
|
destWidth = (int)(srcWidth * targetScale);
|
||||||
|
destHeight = (int)(srcHeight * targetScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose JPEGDEC built-in scaling for coarse downscaling.
|
||||||
|
// Progressive JPEGs: JPEGDEC forces JPEG_SCALE_EIGHTH internally (DC-only
|
||||||
|
// decode produces 1/8 resolution). We must match this to avoid the if/else
|
||||||
|
// priority chain in DecodeJPEG selecting a different scale.
|
||||||
|
int jpegScaleOption;
|
||||||
|
int jpegScaleDenom;
|
||||||
|
if (isProgressive) {
|
||||||
|
jpegScaleOption = JPEG_SCALE_EIGHTH;
|
||||||
|
jpegScaleDenom = 8;
|
||||||
|
} else {
|
||||||
|
jpegScaleDenom = chooseJpegScale(targetScale, jpegScaleOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destWidth <= 0 || destHeight <= 0) {
|
||||||
|
LOG_ERR("JPG", "Degenerate output dimensions %dx%d for %s, skipping render", destWidth, destHeight,
|
||||||
|
imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.scaledSrcWidth = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||||
|
ctx.scaledSrcHeight = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||||
|
ctx.dstWidth = destWidth;
|
||||||
|
ctx.dstHeight = destHeight;
|
||||||
|
ctx.fineScaleFPX = (int32_t)((int64_t)destWidth * FP_ONE / ctx.scaledSrcWidth);
|
||||||
|
ctx.invScaleFPX = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth);
|
||||||
|
ctx.fineScaleFPY = (int32_t)((int64_t)destHeight * FP_ONE / ctx.scaledSrcHeight);
|
||||||
|
ctx.invScaleFPY = (int32_t)((int64_t)ctx.scaledSrcHeight * FP_ONE / destHeight);
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2f)%s", srcWidth, srcHeight, destWidth,
|
||||||
|
destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth,
|
||||||
|
isProgressive ? " [progressive]" : "");
|
||||||
|
|
||||||
|
// Set pixel type to 8-bit grayscale (must be after open())
|
||||||
|
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||||
|
jpeg->setUserPointer(&ctx);
|
||||||
|
|
||||||
|
// Start streaming the pixel cache to disk. The band only needs to hold the
|
||||||
|
// tallest single decode block: a JPEGDEC MCU cell is at most 16 scaled-source
|
||||||
|
// rows tall, which our fine scale maps to this many output rows.
|
||||||
|
ctx.caching = !config.cachePath.empty();
|
||||||
|
if (ctx.caching) {
|
||||||
|
const int maxBlockDstRows = (int)(((int64_t)16 * ctx.fineScaleFPY) >> FP_SHIFT) + 2;
|
||||||
|
if (!ctx.cache.begin(config.cachePath, destWidth, destHeight, config.x, config.y, maxBlockDstRows)) {
|
||||||
|
LOG_ERR("JPG", "Failed to start cache stream, continuing without caching");
|
||||||
|
ctx.caching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long decodeStart = millis();
|
||||||
|
rc = jpeg->decode(0, 0, jpegScaleOption);
|
||||||
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
|
if (rc != 1) {
|
||||||
|
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
|
||||||
|
if (ctx.caching) ctx.cache.abort();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
|
||||||
|
|
||||||
|
// Finalize the streamed cache file. Note: a flush failure mid-decode clears
|
||||||
|
// ctx.caching (the partial file is dropped), so re-read the flag here.
|
||||||
|
if (ctx.caching) {
|
||||||
|
ctx.cache.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
|
return FsHelpers::hasJpgExtension(extension);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||||
|
|
||||||
|
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||||
|
|
||||||
|
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||||
|
return getDimensionsStatic(imagePath, dims);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool supportsFormat(const std::string& extension);
|
||||||
|
const char* getFormatName() const override { return "JPEG"; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Streaming cache writer for 2-bit pixels (4 levels). Packs 4 pixels per byte,
|
||||||
|
// MSB first.
|
||||||
|
//
|
||||||
|
// The .pxc file is written incrementally in small row bands rather than holding
|
||||||
|
// the whole decoded image in one heap buffer. A full-page image (e.g. 482x728)
|
||||||
|
// needs ~88KB packed, which will not fit alongside the ~20KB JPEG decoder on a
|
||||||
|
// fragmented 380KB heap (free heap is routinely ~55KB on an image page). When
|
||||||
|
// the cache cannot be written, every render pass re-decodes the JPEG from
|
||||||
|
// scratch; an anti-aliased image page renders ~14 times (BW + AA restore + two
|
||||||
|
// grayscale planes x ~6 strips), so a 2s decode becomes a ~30s freeze / watchdog
|
||||||
|
// reset. Streaming keeps the working set to a single MCU-row band, so caching
|
||||||
|
// succeeds and the image is decoded exactly once.
|
||||||
|
//
|
||||||
|
// Correctness relies on JPEGDEC delivering blocks in raster MCU order (outer
|
||||||
|
// loop over y, inner over x: see jpeg.inl DecodeJPEG). Consecutive MCU rows map
|
||||||
|
// to contiguous, non-overlapping destination row ranges, so once a block whose
|
||||||
|
// top row is Y arrives, every output row < Y is final and is flushed to disk.
|
||||||
|
struct PixelCache {
|
||||||
|
uint8_t* buffer; // band buffer: (bandRows + 1) rows; last row kept zeroed
|
||||||
|
uint8_t* zeroRow; // points at the spare zeroed row, for gap/clip fill
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int bytesPerRow;
|
||||||
|
int originX; // config.x - to convert screen coords to cache coords
|
||||||
|
int originY; // config.y
|
||||||
|
int bandRows; // rows held in the band buffer
|
||||||
|
int bandStart; // image-local row index of band buffer row 0
|
||||||
|
int flushedRows; // image-local rows already written to file
|
||||||
|
HalFile file;
|
||||||
|
std::string cachePathStr;
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
PixelCache()
|
||||||
|
: buffer(nullptr),
|
||||||
|
zeroRow(nullptr),
|
||||||
|
width(0),
|
||||||
|
height(0),
|
||||||
|
bytesPerRow(0),
|
||||||
|
originX(0),
|
||||||
|
originY(0),
|
||||||
|
bandRows(0),
|
||||||
|
bandStart(0),
|
||||||
|
flushedRows(0),
|
||||||
|
ok(false) {}
|
||||||
|
PixelCache(const PixelCache&) = delete;
|
||||||
|
PixelCache& operator=(const PixelCache&) = delete;
|
||||||
|
|
||||||
|
static constexpr int MIN_BAND_ROWS = 16;
|
||||||
|
static constexpr size_t MAX_BAND_BYTES = 24 * 1024; // band working-set ceiling
|
||||||
|
|
||||||
|
// Open the cache file, write the header, and allocate a band buffer big enough
|
||||||
|
// to hold the tallest single decode block (maxBlockDstRows output rows).
|
||||||
|
bool begin(const std::string& cachePath, int w, int h, int ox, int oy, int maxBlockDstRows) {
|
||||||
|
width = w;
|
||||||
|
height = h;
|
||||||
|
originX = ox;
|
||||||
|
originY = oy;
|
||||||
|
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||||
|
bandStart = 0;
|
||||||
|
flushedRows = 0;
|
||||||
|
ok = false;
|
||||||
|
|
||||||
|
int wantRows = maxBlockDstRows + 2;
|
||||||
|
if (wantRows < MIN_BAND_ROWS) wantRows = MIN_BAND_ROWS;
|
||||||
|
if (wantRows > h) wantRows = h;
|
||||||
|
|
||||||
|
size_t maxRowsByMem = MAX_BAND_BYTES / (size_t)bytesPerRow;
|
||||||
|
if (maxRowsByMem < 1) maxRowsByMem = 1;
|
||||||
|
if ((size_t)wantRows > maxRowsByMem) wantRows = (int)maxRowsByMem;
|
||||||
|
|
||||||
|
// A single decode block must fit inside the band, otherwise streaming would
|
||||||
|
// drop rows. This only fails for pathological upscales that could not be
|
||||||
|
// cached at all; fall back to the no-cache path.
|
||||||
|
if (wantRows < maxBlockDstRows) {
|
||||||
|
LOG_ERR("IMG", "Cache band too small (%d < %d rows) for %dx%d", wantRows, maxBlockDstRows, w, h);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bandRows = wantRows;
|
||||||
|
|
||||||
|
const size_t bufSize = (size_t)(bandRows + 1) * bytesPerRow; // +1 spare zero row
|
||||||
|
buffer = (uint8_t*)malloc(bufSize);
|
||||||
|
if (!buffer) {
|
||||||
|
LOG_ERR("IMG", "OOM cache band: %u bytes", (unsigned)bufSize);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memset(buffer, 0, bufSize);
|
||||||
|
zeroRow = buffer + (size_t)bandRows * bytesPerRow;
|
||||||
|
|
||||||
|
if (!Storage.openFileForWrite("IMG", cachePath, file)) {
|
||||||
|
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||||
|
free(buffer);
|
||||||
|
buffer = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cachePathStr = cachePath;
|
||||||
|
|
||||||
|
uint16_t w16 = (uint16_t)w;
|
||||||
|
uint16_t h16 = (uint16_t)h;
|
||||||
|
if (file.write(&w16, 2) != 2 || file.write(&h16, 2) != 2) {
|
||||||
|
LOG_ERR("IMG", "Failed to write cache header: %s", cachePath.c_str());
|
||||||
|
abort();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Cache stream started: %s (%dx%d, band %d rows)", cachePath.c_str(), w, h, bandRows);
|
||||||
|
ok = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush every output row below newTopRow (they are final in raster order) and
|
||||||
|
// reposition the band to start at newTopRow. Returns false if a write failed,
|
||||||
|
// in which case the caller must stop caching for the rest of the decode.
|
||||||
|
bool advanceTo(int newTopRow) {
|
||||||
|
if (!ok) return false;
|
||||||
|
if (newTopRow <= bandStart) return true;
|
||||||
|
if (newTopRow > height) newTopRow = height;
|
||||||
|
|
||||||
|
for (int r = bandStart; r < newTopRow; ++r) {
|
||||||
|
const int idx = r - bandStart;
|
||||||
|
const uint8_t* rowPtr = (idx < bandRows) ? (buffer + (size_t)idx * bytesPerRow) : zeroRow;
|
||||||
|
if (file.write(rowPtr, (size_t)bytesPerRow) != (size_t)bytesPerRow) {
|
||||||
|
LOG_ERR("IMG", "Cache write error at row %d", r);
|
||||||
|
ok = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushedRows = newTopRow;
|
||||||
|
bandStart = newTopRow;
|
||||||
|
memset(buffer, 0, (size_t)bandRows * bytesPerRow); // fresh band (gaps stay black)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the final band and zero-fill any rows never covered (image clipped by
|
||||||
|
// the screen), then close the file.
|
||||||
|
bool finalize() {
|
||||||
|
if (!ok) {
|
||||||
|
abort();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int r = flushedRows; r < height; ++r) {
|
||||||
|
const int idx = r - bandStart;
|
||||||
|
const uint8_t* rowPtr = (idx >= 0 && idx < bandRows) ? (buffer + (size_t)idx * bytesPerRow) : zeroRow;
|
||||||
|
if (file.write(rowPtr, (size_t)bytesPerRow) != (size_t)bytesPerRow) {
|
||||||
|
LOG_ERR("IMG", "Cache write error at row %d", r);
|
||||||
|
abort();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePathStr.c_str(), width, height,
|
||||||
|
4 + bytesPerRow * height);
|
||||||
|
ok = false; // file handed off; nothing left to clean up
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop a partial/failed cache so a later decode re-creates it cleanly.
|
||||||
|
void abort() {
|
||||||
|
if (file.isOpen()) file.close();
|
||||||
|
if (!cachePathStr.empty()) {
|
||||||
|
Storage.remove(cachePathStr.c_str());
|
||||||
|
}
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
~PixelCache() {
|
||||||
|
if (file.isOpen()) {
|
||||||
|
// The file is still open, so neither finalize() nor abort() ran, or a
|
||||||
|
// mid-stream write failed (advanceTo() cleared ok but left the file open).
|
||||||
|
// Drop the partial cache so we leave no corrupt file behind.
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
if (buffer) {
|
||||||
|
free(buffer);
|
||||||
|
buffer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
#include "PngToFramebufferConverter.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
|
#include <PNGdec.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
#include "DirectPixelWriter.h"
|
||||||
|
#include "DitherUtils.h"
|
||||||
|
#include "PixelCache.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Context struct passed through PNGdec callbacks to avoid global mutable state.
|
||||||
|
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||||
|
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by pngOpen()).
|
||||||
|
struct PngContext {
|
||||||
|
GfxRenderer* renderer{nullptr};
|
||||||
|
const RenderConfig* config{nullptr};
|
||||||
|
int screenWidth{0};
|
||||||
|
int screenHeight{0};
|
||||||
|
|
||||||
|
// Scaling state
|
||||||
|
float scale{1.f};
|
||||||
|
int srcWidth{0};
|
||||||
|
int srcHeight{0};
|
||||||
|
int dstWidth{0};
|
||||||
|
int dstHeight{0};
|
||||||
|
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
|
||||||
|
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching{false};
|
||||||
|
|
||||||
|
uint8_t* grayLineBuffer{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||||
|
// avoiding the need for global file state.
|
||||||
|
void* pngOpenWithHandle(const char* filename, int32_t* size) {
|
||||||
|
HalFile* f = new HalFile();
|
||||||
|
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
|
||||||
|
delete f;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
*size = f->size();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pngCloseWithHandle(void* handle) {
|
||||||
|
HalFile* f = reinterpret_cast<HalFile*>(handle);
|
||||||
|
if (f) {
|
||||||
|
f->close();
|
||||||
|
delete f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||||
|
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||||
|
if (!f) return 0;
|
||||||
|
return f->read(pBuf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
||||||
|
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||||
|
if (!f) return -1;
|
||||||
|
return f->seek(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
|
||||||
|
// We heap-allocate it on demand rather than using a static instance, so this memory
|
||||||
|
// is only consumed while actually decoding/querying PNG images. This is critical on
|
||||||
|
// the ESP32-C3 where total RAM is ~320 KB.
|
||||||
|
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||||
|
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||||
|
|
||||||
|
// PNGdec keeps TWO scanlines in its internal ucPixels buffer (current + previous)
|
||||||
|
// and each scanline includes a leading filter byte.
|
||||||
|
// Required storage is therefore approximately: 2 * (pitch + 1) + alignment slack.
|
||||||
|
// If PNG_MAX_BUFFERED_PIXELS is smaller than this requirement for a given image,
|
||||||
|
// PNGdec can overrun its internal buffer before our draw callback executes.
|
||||||
|
int bytesPerPixelFromType(int pixelType) {
|
||||||
|
switch (pixelType) {
|
||||||
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
|
return 3;
|
||||||
|
case PNG_PIXEL_GRAY_ALPHA:
|
||||||
|
return 2;
|
||||||
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
|
return 4;
|
||||||
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
|
case PNG_PIXEL_INDEXED:
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; }
|
||||||
|
|
||||||
|
int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) {
|
||||||
|
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||||
|
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||||
|
if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) {
|
||||||
|
pitch = packedRowBytes(srcWidth, bitsPerSample);
|
||||||
|
}
|
||||||
|
return ((pitch + 1) * 2) + 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSupportedBitDepth(int pixelType, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return true;
|
||||||
|
if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false;
|
||||||
|
return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return pixels[x];
|
||||||
|
|
||||||
|
const int bitOffset = x * bitsPerSample;
|
||||||
|
const int shift = 8 - bitsPerSample - (bitOffset & 7);
|
||||||
|
const uint8_t mask = (1U << bitsPerSample) - 1;
|
||||||
|
return (pixels[bitOffset >> 3] >> shift) & mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
|
||||||
|
if (bitsPerSample == 8) return sample;
|
||||||
|
const uint8_t maxSample = (1U << bitsPerSample) - 1;
|
||||||
|
return static_cast<uint8_t>((sample * 255U) / maxSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert entire source line to grayscale with alpha blending to white background.
|
||||||
|
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
|
||||||
|
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||||
|
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||||
|
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
|
||||||
|
uint8_t* palette, int hasAlpha) {
|
||||||
|
switch (pixelType) {
|
||||||
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
|
if (bitsPerSample == 8) {
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
const uint8_t* p = &pPixels[x * 3];
|
||||||
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_INDEXED:
|
||||||
|
if (palette) {
|
||||||
|
if (hasAlpha) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||||
|
uint8_t* p = &palette[idx * 3];
|
||||||
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
uint8_t alpha = palette[768 + idx];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||||
|
uint8_t* p = &palette[idx * 3];
|
||||||
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_GRAY_ALPHA:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t gray = pPixels[x * 2];
|
||||||
|
uint8_t alpha = pPixels[x * 2 + 1];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
const uint8_t* p = &pPixels[x * 4];
|
||||||
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
uint8_t alpha = p[3];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
memset(grayLine, 128, width);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||||
|
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||||
|
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||||
|
|
||||||
|
int srcY = pDraw->y;
|
||||||
|
int srcWidth = ctx->srcWidth;
|
||||||
|
|
||||||
|
// Map source rows with the exact output-height ratio. During downscaling,
|
||||||
|
// multiple source rows can select the same output row; during upscaling, one
|
||||||
|
// source row must be repeated across every output row in its range. Emitting
|
||||||
|
// only the first row of an upscale leaves zero-filled (black) gaps in the
|
||||||
|
// streamed pixel cache.
|
||||||
|
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
|
||||||
|
int endDstY = firstDstY + 1;
|
||||||
|
if (ctx->dstHeight > ctx->srcHeight) {
|
||||||
|
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||||
|
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||||
|
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||||
|
|
||||||
|
// Convert entire source line to grayscale (improves cache locality)
|
||||||
|
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
|
||||||
|
pDraw->iHasAlpha);
|
||||||
|
|
||||||
|
// Render scaled rows using Bresenham-style integer stepping (no floating-point division)
|
||||||
|
int dstWidth = ctx->dstWidth;
|
||||||
|
int outXBase = ctx->config->x;
|
||||||
|
int screenWidth = ctx->screenWidth;
|
||||||
|
bool useDithering = ctx->config->useDithering;
|
||||||
|
|
||||||
|
// Pre-compute orientation and render-mode state once per callback.
|
||||||
|
DirectPixelWriter pw;
|
||||||
|
pw.init(*ctx->renderer);
|
||||||
|
|
||||||
|
for (int dstY = firstDstY; dstY < endDstY; dstY++) {
|
||||||
|
ctx->lastDstY = dstY;
|
||||||
|
int outY = ctx->config->y + dstY;
|
||||||
|
if (outY >= ctx->screenHeight) continue;
|
||||||
|
|
||||||
|
pw.beginRow(outY);
|
||||||
|
|
||||||
|
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||||
|
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||||
|
// A flush failure stops caching for the rest of the decode so we never write
|
||||||
|
// past the band buffer; finalize() then drops the partial file.
|
||||||
|
bool caching = ctx->caching;
|
||||||
|
DirectCacheWriter cw;
|
||||||
|
if (caching) {
|
||||||
|
if (!ctx->cache.advanceTo(dstY)) {
|
||||||
|
caching = false;
|
||||||
|
ctx->caching = false;
|
||||||
|
} else {
|
||||||
|
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||||
|
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int srcX = 0;
|
||||||
|
int error = 0;
|
||||||
|
|
||||||
|
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||||
|
int outX = outXBase + dstX;
|
||||||
|
if (outX < screenWidth) {
|
||||||
|
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||||
|
|
||||||
|
uint8_t ditheredGray;
|
||||||
|
if (useDithering) {
|
||||||
|
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
ditheredGray = gray / 85;
|
||||||
|
if (ditheredGray > 3) ditheredGray = 3;
|
||||||
|
}
|
||||||
|
pw.writePixel(outX, ditheredGray);
|
||||||
|
if (caching) cw.writePixel(outX, ditheredGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||||
|
error += srcWidth;
|
||||||
|
while (error >= dstWidth) {
|
||||||
|
error -= dstWidth;
|
||||||
|
srcX++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||||
|
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
|
||||||
|
if (!png) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||||
|
nullptr);
|
||||||
|
const ScopedCleanup cleanup{[&png]() { png->close(); }};
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = png->getWidth();
|
||||||
|
out.height = png->getHeight();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||||
|
const RenderConfig& config) {
|
||||||
|
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||||
|
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
|
||||||
|
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
|
||||||
|
if (!png) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate PNG decoder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PngContext ctx;
|
||||||
|
ctx.renderer = &renderer;
|
||||||
|
ctx.config = &config;
|
||||||
|
ctx.screenWidth = renderer.getScreenWidth();
|
||||||
|
ctx.screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||||
|
pngDrawCallback);
|
||||||
|
const ScopedCleanup cleanup{[&png]() { png->close(); }};
|
||||||
|
if (rc != PNG_SUCCESS) {
|
||||||
|
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output dimensions
|
||||||
|
ctx.srcWidth = png->getWidth();
|
||||||
|
ctx.srcHeight = png->getHeight();
|
||||||
|
|
||||||
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
|
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||||
|
ctx.dstWidth = config.maxWidth;
|
||||||
|
ctx.dstHeight = config.maxHeight;
|
||||||
|
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||||
|
} else {
|
||||||
|
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||||
|
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||||
|
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||||
|
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||||
|
|
||||||
|
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||||
|
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||||
|
}
|
||||||
|
ctx.lastDstY = -1; // Reset row tracking
|
||||||
|
|
||||||
|
const int pixelType = png->getPixelType();
|
||||||
|
const int bitsPerSample = png->getBpp();
|
||||||
|
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
|
||||||
|
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||||
|
|
||||||
|
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
|
||||||
|
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||||
|
LOG_ERR(
|
||||||
|
"PNG",
|
||||||
|
"PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||||
|
requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS);
|
||||||
|
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
|
||||||
|
warnUnsupportedFeature(
|
||||||
|
"bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The converter expands each source row to 8-bit grayscale before dithering,
|
||||||
|
// so this scratch buffer is sized by source pixels even when PNGdec reads a
|
||||||
|
// packed 1/2/4-bit row internally.
|
||||||
|
constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||||
|
const size_t grayBufSize = static_cast<size_t>(ctx.srcWidth);
|
||||||
|
if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) {
|
||||||
|
LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast<unsigned>(grayBufSize),
|
||||||
|
ctx.srcWidth, static_cast<unsigned>(MAX_GRAY_LINE_BUFFER_BYTES));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
|
||||||
|
if (!grayLineBuffer) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||||
|
|
||||||
|
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||||
|
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||||
|
// band only needs a single row. Streaming keeps the working set tiny, so
|
||||||
|
// unlike the old full-image buffer it neither competes with the ~44KB decoder
|
||||||
|
// nor forces larger images to skip caching - which previously meant a full
|
||||||
|
// re-decode on every one of an image page's ~14 render passes.
|
||||||
|
ctx.caching = !config.cachePath.empty();
|
||||||
|
if (ctx.caching) {
|
||||||
|
if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) {
|
||||||
|
LOG_ERR("PNG", "Failed to start cache stream, continuing without caching");
|
||||||
|
ctx.caching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long decodeStart = millis();
|
||||||
|
rc = png->decode(&ctx, 0);
|
||||||
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
|
ctx.grayLineBuffer = nullptr;
|
||||||
|
|
||||||
|
if (rc != PNG_SUCCESS) {
|
||||||
|
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||||
|
if (ctx.caching) ctx.cache.abort();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
|
||||||
|
|
||||||
|
// Finalize the streamed cache (caching may have been cleared on a flush error).
|
||||||
|
if (ctx.caching) {
|
||||||
|
ctx.cache.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
|
return FsHelpers::hasPngExtension(extension);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class PngToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||||
|
|
||||||
|
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||||
|
|
||||||
|
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||||
|
return getDimensionsStatic(imagePath, dims);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool supportsFormat(const std::string& extension);
|
||||||
|
const char* getFormatName() const override { return "PNG"; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,956 @@
|
|||||||
|
#include "CssParser.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Stack-allocated string buffer to avoid heap reallocations during parsing
|
||||||
|
// Provides string-like interface with fixed capacity
|
||||||
|
struct StackBuffer {
|
||||||
|
static constexpr size_t CAPACITY = 1024;
|
||||||
|
char data[CAPACITY];
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
void push_back(char c) {
|
||||||
|
if (len < CAPACITY - 1) {
|
||||||
|
data[len++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() { len = 0; }
|
||||||
|
bool empty() const { return len == 0; }
|
||||||
|
size_t size() const { return len; }
|
||||||
|
|
||||||
|
// Get string view of current content (zero-copy)
|
||||||
|
std::string_view view() const { return std::string_view(data, len); }
|
||||||
|
operator std::string_view() const noexcept { return view(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buffer size for reading CSS files
|
||||||
|
constexpr size_t READ_BUFFER_SIZE = 512;
|
||||||
|
|
||||||
|
// Maximum number of CSS rules to store in the selector map
|
||||||
|
// Prevents unbounded memory growth from pathological CSS files
|
||||||
|
constexpr size_t MAX_RULES = 1500;
|
||||||
|
|
||||||
|
// Minimum free heap required to apply CSS during rendering
|
||||||
|
// If below this threshold, we skip CSS to avoid display artifacts.
|
||||||
|
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
|
||||||
|
|
||||||
|
// Maximum length for a single selector string
|
||||||
|
// Prevents parsing of extremely long or malformed selectors
|
||||||
|
constexpr size_t MAX_SELECTOR_LENGTH = 256;
|
||||||
|
|
||||||
|
// Check if character is CSS whitespace
|
||||||
|
constexpr bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||||
|
|
||||||
|
constexpr std::string_view trimCssWhitespace(std::string_view s) {
|
||||||
|
while (!s.empty() && isCssWhitespace(s.front())) s.remove_prefix(1);
|
||||||
|
while (!s.empty() && isCssWhitespace(s.back())) s.remove_suffix(1);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr char asciiToLower(const char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c; }
|
||||||
|
|
||||||
|
// Case-insensitive equality on ASCII. lowercaseKeyword MUST already be
|
||||||
|
// lowercase; CSS keywords are ASCII by spec so byte-wise tolower is safe.
|
||||||
|
constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKeyword) {
|
||||||
|
return std::equal(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
|
||||||
|
[](char a, char b) { return asciiToLower(a) == b; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk s and invoke fn(token) for each non-empty run between delimiters.
|
||||||
|
// Tokens are boundary-trimmed and yielded as string_views into s; no
|
||||||
|
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
|
||||||
|
// emitted. `isDelimiter` is invoked once per character.
|
||||||
|
template <typename Pred, typename F>
|
||||||
|
void forEachDelimitedToken(std::string_view s, Pred isDelimiter, F&& fn) {
|
||||||
|
size_t start = 0;
|
||||||
|
for (size_t i = 0; i <= s.size(); ++i) {
|
||||||
|
if (i == s.size() || isDelimiter(s[i])) {
|
||||||
|
const std::string_view trimmed = trimCssWhitespace(s.substr(start, i - start));
|
||||||
|
if (!trimmed.empty()) {
|
||||||
|
fn(trimmed);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FNV-1a per Fowler/Noll/Vo, sized to match size_t on the target. The firmware
|
||||||
|
// runs on a 32-bit core where size_t is 32 bits, so naively using the 64-bit
|
||||||
|
// constants would silently truncate FNV_PRIME to a non-prime and wreck hash
|
||||||
|
// distribution. The selection below picks the canonical 32- or 64-bit
|
||||||
|
// constants at compile time so the same source works in a 64-bit host
|
||||||
|
// simulator. `fnv1aMix` is the per-byte mix step; callers apply any
|
||||||
|
// byte-level transform (e.g. asciiToLower) first.
|
||||||
|
static_assert(sizeof(size_t) == 4 || sizeof(size_t) == 8, "FNV constants are only defined for 32- or 64-bit size_t");
|
||||||
|
constexpr size_t FNV_OFFSET_BASIS =
|
||||||
|
sizeof(size_t) == 8 ? static_cast<size_t>(14695981039346656037ULL) : static_cast<size_t>(2166136261U);
|
||||||
|
constexpr size_t FNV_PRIME =
|
||||||
|
sizeof(size_t) == 8 ? static_cast<size_t>(1099511628211ULL) : static_cast<size_t>(16777619U);
|
||||||
|
|
||||||
|
constexpr size_t fnv1aMix(size_t hash, unsigned char byte) { return (hash ^ byte) * FNV_PRIME; }
|
||||||
|
|
||||||
|
// Parse the entirety of s as a number into `out`. Accepts an optional leading
|
||||||
|
// '+' (which std::from_chars rejects by spec) so callers can pass CSS-style
|
||||||
|
// signed numbers without manual trimming. Returns false on empty input, a
|
||||||
|
// non-numeric suffix, or any from_chars error.
|
||||||
|
template <typename T>
|
||||||
|
bool tryParseNumber(std::string_view s, T& out) {
|
||||||
|
const char* begin = s.data();
|
||||||
|
const char* end = s.data() + s.size();
|
||||||
|
if (begin < end && *begin == '+') ++begin;
|
||||||
|
const auto r = std::from_chars(begin, end, out);
|
||||||
|
return r.ec == std::errc{} && r.ptr == end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect up to 4 whitespace-separated tokens for a CSS edge-value shorthand
|
||||||
|
// (margin, padding, and the border-* family). Returns the number of tokens
|
||||||
|
// written; extras are silently dropped. Callers apply the 1/2/3/4-value
|
||||||
|
// fallback rule using the returned count.
|
||||||
|
size_t collectEdgeValueTokens(std::string_view s, std::string_view (&out)[4]) {
|
||||||
|
size_t count = 0;
|
||||||
|
forEachDelimitedToken(s, isCssWhitespace, [&](std::string_view tok) {
|
||||||
|
if (count < 4) out[count++] = tok;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view stripTrailingImportant(std::string_view value) {
|
||||||
|
constexpr std::string_view IMPORTANT = "!important";
|
||||||
|
|
||||||
|
while (!value.empty() && isCssWhitespace(value.back())) {
|
||||||
|
value.remove_suffix(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.size() < IMPORTANT.size()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t suffixPos = value.size() - IMPORTANT.size();
|
||||||
|
if (!iequalsAscii(value.substr(suffixPos), IMPORTANT)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.remove_suffix(IMPORTANT.size());
|
||||||
|
while (!value.empty() && isCssWhitespace(value.back())) {
|
||||||
|
value.remove_suffix(1);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
// Transparent case-insensitive hash/equal. Bodies live here (rather than
|
||||||
|
// inline in the header) so they can share the anonymous-namespace asciiToLower
|
||||||
|
// with the other ASCII helpers in this translation unit.
|
||||||
|
|
||||||
|
size_t CssParser::SvHash::operator()(std::string_view sv) const noexcept {
|
||||||
|
size_t h = FNV_OFFSET_BASIS;
|
||||||
|
for (char c : sv) h = fnv1aMix(h, asciiToLower(c));
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t CssParser::SvHash::operator()(const std::string& s) const noexcept { return operator()(std::string_view(s)); }
|
||||||
|
|
||||||
|
size_t CssParser::SvHash::operator()(CompositeKey k) const noexcept {
|
||||||
|
// Hash the case-folded concatenation of every piece without materializing
|
||||||
|
// it — the running hash continues across pieces as if they were one buffer.
|
||||||
|
size_t h = FNV_OFFSET_BASIS;
|
||||||
|
for (std::string_view piece : k.pieces) {
|
||||||
|
for (char c : piece) h = fnv1aMix(h, asciiToLower(c));
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::SvEqual::operator()(std::string_view a, std::string_view b) const noexcept {
|
||||||
|
if (a.size() != b.size()) return false;
|
||||||
|
for (size_t i = 0; i < a.size(); ++i) {
|
||||||
|
if (asciiToLower(a[i]) != asciiToLower(b[i])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::SvEqual::operator()(const std::string& a, std::string_view b) const noexcept {
|
||||||
|
return operator()(std::string_view(a), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::SvEqual::operator()(std::string_view a, const std::string& b) const noexcept {
|
||||||
|
return operator()(a, std::string_view(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::SvEqual::operator()(const std::string& a, const std::string& b) const noexcept {
|
||||||
|
return operator()(std::string_view(a), std::string_view(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::SvEqual::operator()(CompositeKey k, std::string_view sv) const noexcept {
|
||||||
|
size_t total = 0;
|
||||||
|
for (std::string_view piece : k.pieces) total += piece.size();
|
||||||
|
if (total != sv.size()) return false;
|
||||||
|
size_t i = 0;
|
||||||
|
for (std::string_view piece : k.pieces) {
|
||||||
|
for (char c : piece) {
|
||||||
|
if (asciiToLower(c) != asciiToLower(sv[i++])) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::SvEqual::operator()(std::string_view sv, CompositeKey k) const noexcept { return operator()(k, sv); }
|
||||||
|
|
||||||
|
// Property value interpreters
|
||||||
|
|
||||||
|
CssTextAlign CssParser::interpretAlignment(std::string_view val) {
|
||||||
|
val = trimCssWhitespace(val);
|
||||||
|
|
||||||
|
if (iequalsAscii(val, "left") || iequalsAscii(val, "start")) return CssTextAlign::Left;
|
||||||
|
if (iequalsAscii(val, "right") || iequalsAscii(val, "end")) return CssTextAlign::Right;
|
||||||
|
if (iequalsAscii(val, "center")) return CssTextAlign::Center;
|
||||||
|
if (iequalsAscii(val, "justify")) return CssTextAlign::Justify;
|
||||||
|
|
||||||
|
return CssTextAlign::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssFontStyle CssParser::interpretFontStyle(std::string_view val) {
|
||||||
|
val = trimCssWhitespace(val);
|
||||||
|
|
||||||
|
if (iequalsAscii(val, "italic") || iequalsAscii(val, "oblique")) return CssFontStyle::Italic;
|
||||||
|
return CssFontStyle::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
|
||||||
|
val = trimCssWhitespace(val);
|
||||||
|
|
||||||
|
// Named values
|
||||||
|
if (iequalsAscii(val, "bold") || iequalsAscii(val, "bolder")) return CssFontWeight::Bold;
|
||||||
|
if (iequalsAscii(val, "normal") || iequalsAscii(val, "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)
|
||||||
|
long numericWeight = 0;
|
||||||
|
if (tryParseNumber(val, numericWeight)) {
|
||||||
|
return numericWeight >= 700 ? CssFontWeight::Bold : CssFontWeight::Normal;
|
||||||
|
}
|
||||||
|
return CssFontWeight::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
|
||||||
|
// text-decoration can have multiple space-separated values. Compare whole tokens
|
||||||
|
// so malformed values like "notunderline" do not accidentally enable a line.
|
||||||
|
CssTextDecoration result = CssTextDecoration::None;
|
||||||
|
bool explicitNone = false;
|
||||||
|
forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
|
||||||
|
if (iequalsAscii(token, "none")) {
|
||||||
|
explicitNone = true;
|
||||||
|
} else if (iequalsAscii(token, "underline")) {
|
||||||
|
result = result | CssTextDecoration::Underline;
|
||||||
|
} else if (iequalsAscii(token, "line-through")) {
|
||||||
|
result = result | CssTextDecoration::LineThrough;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return explicitNone ? CssTextDecoration::None : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CssLength CssParser::interpretLength(std::string_view val) {
|
||||||
|
CssLength result;
|
||||||
|
tryInterpretLength(val, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::tryInterpretLength(std::string_view val, CssLength& out) {
|
||||||
|
val = trimCssWhitespace(val);
|
||||||
|
if (val.empty()) {
|
||||||
|
out = CssLength{};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t unitStart = val.size();
|
||||||
|
for (size_t i = 0; i < val.size(); ++i) {
|
||||||
|
const char c = val[i];
|
||||||
|
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||||
|
unitStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float numericValue;
|
||||||
|
if (!tryParseNumber(val.substr(0, unitStart), numericValue)) {
|
||||||
|
out = CssLength{};
|
||||||
|
return false; // No number parsed (e.g. auto, inherit, initial)
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string_view unitPart = val.substr(unitStart);
|
||||||
|
auto unit = CssUnit::Pixels;
|
||||||
|
if (iequalsAscii(unitPart, "em")) {
|
||||||
|
unit = CssUnit::Em;
|
||||||
|
} else if (iequalsAscii(unitPart, "rem")) {
|
||||||
|
unit = CssUnit::Rem;
|
||||||
|
} else if (iequalsAscii(unitPart, "pt")) {
|
||||||
|
unit = CssUnit::Points;
|
||||||
|
} else if (unitPart == "%") {
|
||||||
|
unit = CssUnit::Percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = CssLength{numericValue, unit};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declaration parsing
|
||||||
|
|
||||||
|
void CssParser::parseDeclarationIntoStyle(std::string_view decl, CssStyle& style) {
|
||||||
|
const size_t colonPos = decl.find(':');
|
||||||
|
if (colonPos == std::string_view::npos || colonPos == 0) return;
|
||||||
|
|
||||||
|
const std::string_view name = trimCssWhitespace(decl.substr(0, colonPos));
|
||||||
|
const std::string_view value = trimCssWhitespace(decl.substr(colonPos + 1));
|
||||||
|
|
||||||
|
if (name.empty() || value.empty()) return;
|
||||||
|
|
||||||
|
if (iequalsAscii(name, "text-align")) {
|
||||||
|
style.textAlign = interpretAlignment(value);
|
||||||
|
style.defined.textAlign = 1;
|
||||||
|
} else if (iequalsAscii(name, "font-style")) {
|
||||||
|
style.fontStyle = interpretFontStyle(value);
|
||||||
|
style.defined.fontStyle = 1;
|
||||||
|
} else if (iequalsAscii(name, "font-weight")) {
|
||||||
|
style.fontWeight = interpretFontWeight(value);
|
||||||
|
style.defined.fontWeight = 1;
|
||||||
|
} else if (iequalsAscii(name, "text-decoration") || iequalsAscii(name, "text-decoration-line")) {
|
||||||
|
style.textDecoration = interpretDecoration(value);
|
||||||
|
style.defined.textDecoration = 1;
|
||||||
|
} else if (iequalsAscii(name, "text-indent")) {
|
||||||
|
style.textIndent = interpretLength(value);
|
||||||
|
style.defined.textIndent = 1;
|
||||||
|
} else if (iequalsAscii(name, "margin-top")) {
|
||||||
|
style.marginTop = interpretLength(value);
|
||||||
|
style.defined.marginTop = 1;
|
||||||
|
} else if (iequalsAscii(name, "margin-bottom")) {
|
||||||
|
style.marginBottom = interpretLength(value);
|
||||||
|
style.defined.marginBottom = 1;
|
||||||
|
} else if (iequalsAscii(name, "margin-left")) {
|
||||||
|
style.marginLeft = interpretLength(value);
|
||||||
|
style.defined.marginLeft = 1;
|
||||||
|
} else if (iequalsAscii(name, "margin-right")) {
|
||||||
|
style.marginRight = interpretLength(value);
|
||||||
|
style.defined.marginRight = 1;
|
||||||
|
} else if (iequalsAscii(name, "margin")) {
|
||||||
|
std::string_view margins[4];
|
||||||
|
const size_t count = collectEdgeValueTokens(value, margins);
|
||||||
|
if (count > 0) {
|
||||||
|
style.marginTop = interpretLength(margins[0]);
|
||||||
|
style.marginRight = count >= 2 ? interpretLength(margins[1]) : style.marginTop;
|
||||||
|
style.marginBottom = count >= 3 ? interpretLength(margins[2]) : style.marginTop;
|
||||||
|
style.marginLeft = count >= 4 ? interpretLength(margins[3]) : style.marginRight;
|
||||||
|
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
} else if (iequalsAscii(name, "padding-top")) {
|
||||||
|
style.paddingTop = interpretLength(value);
|
||||||
|
style.defined.paddingTop = 1;
|
||||||
|
} else if (iequalsAscii(name, "padding-bottom")) {
|
||||||
|
style.paddingBottom = interpretLength(value);
|
||||||
|
style.defined.paddingBottom = 1;
|
||||||
|
} else if (iequalsAscii(name, "padding-left")) {
|
||||||
|
style.paddingLeft = interpretLength(value);
|
||||||
|
style.defined.paddingLeft = 1;
|
||||||
|
} else if (iequalsAscii(name, "padding-right")) {
|
||||||
|
style.paddingRight = interpretLength(value);
|
||||||
|
style.defined.paddingRight = 1;
|
||||||
|
} else if (iequalsAscii(name, "padding")) {
|
||||||
|
std::string_view paddings[4];
|
||||||
|
const size_t count = collectEdgeValueTokens(value, paddings);
|
||||||
|
if (count > 0) {
|
||||||
|
style.paddingTop = interpretLength(paddings[0]);
|
||||||
|
style.paddingRight = count >= 2 ? interpretLength(paddings[1]) : style.paddingTop;
|
||||||
|
style.paddingBottom = count >= 3 ? interpretLength(paddings[2]) : style.paddingTop;
|
||||||
|
style.paddingLeft = count >= 4 ? interpretLength(paddings[3]) : style.paddingRight;
|
||||||
|
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
|
||||||
|
1;
|
||||||
|
}
|
||||||
|
} else if (iequalsAscii(name, "height")) {
|
||||||
|
CssLength len;
|
||||||
|
if (tryInterpretLength(value, len)) {
|
||||||
|
style.imageHeight = len;
|
||||||
|
style.defined.imageHeight = 1;
|
||||||
|
}
|
||||||
|
} else if (iequalsAscii(name, "width")) {
|
||||||
|
CssLength len;
|
||||||
|
if (tryInterpretLength(value, len)) {
|
||||||
|
style.imageWidth = len;
|
||||||
|
style.defined.imageWidth = 1;
|
||||||
|
}
|
||||||
|
} else if (iequalsAscii(name, "display")) {
|
||||||
|
const std::string_view displayValue = stripTrailingImportant(value);
|
||||||
|
style.display = iequalsAscii(displayValue, "none") ? CssDisplay::None : CssDisplay::Block;
|
||||||
|
style.defined.display = 1;
|
||||||
|
} else if (iequalsAscii(name, "direction")) {
|
||||||
|
const std::string_view directionValue = stripTrailingImportant(value);
|
||||||
|
if (iequalsAscii(directionValue, "rtl")) {
|
||||||
|
style.direction = CssTextDirection::Rtl;
|
||||||
|
style.defined.direction = 1;
|
||||||
|
} else if (iequalsAscii(directionValue, "ltr")) {
|
||||||
|
style.direction = CssTextDirection::Ltr;
|
||||||
|
style.defined.direction = 1;
|
||||||
|
}
|
||||||
|
} else if (iequalsAscii(name, "vertical-align")) {
|
||||||
|
if (iequalsAscii(value, "super")) {
|
||||||
|
style.verticalAlign = CssVerticalAlign::Super;
|
||||||
|
style.defined.verticalAlign = 1;
|
||||||
|
} else if (iequalsAscii(value, "sub")) {
|
||||||
|
style.verticalAlign = CssVerticalAlign::Sub;
|
||||||
|
style.defined.verticalAlign = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
|
||||||
|
CssStyle style;
|
||||||
|
|
||||||
|
size_t start = 0;
|
||||||
|
for (size_t i = 0; i <= declBlock.size(); ++i) {
|
||||||
|
if (i == declBlock.size() || declBlock[i] == ';') {
|
||||||
|
if (i > start) {
|
||||||
|
parseDeclarationIntoStyle(declBlock.substr(start, i - start), style);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule processing
|
||||||
|
|
||||||
|
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
|
||||||
|
// Skip rules that don't define any supported properties to save RAM.
|
||||||
|
if (!style.defined.anySet()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've reached the rule limit before processing
|
||||||
|
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||||
|
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk comma-separated selectors in place — no vector allocation. Selectors
|
||||||
|
// with unsupported syntax (combinators, attributes, pseudo, etc.) are skipped
|
||||||
|
// silently; the only heap allocation per kept selector is the std::string
|
||||||
|
// map key, which is unavoidable since the map owns its keys.
|
||||||
|
bool limitReached = false;
|
||||||
|
forEachDelimitedToken(
|
||||||
|
selectorGroup, [](char c) { return c == ','; },
|
||||||
|
[&](std::string_view sel) {
|
||||||
|
if (limitReached) return;
|
||||||
|
|
||||||
|
if (sel.size() > MAX_SELECTOR_LENGTH) {
|
||||||
|
LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Support richer CSS selector syntax in the future. For now we only
|
||||||
|
// handle `tag`, `.class`, or `tag.class`. Reject anything containing a
|
||||||
|
// character that introduces unsupported syntax:
|
||||||
|
// '+' adjacent sibling combinator
|
||||||
|
// '>' child combinator
|
||||||
|
// '[' attribute selector
|
||||||
|
// ':' pseudo class/element
|
||||||
|
// '#' ID selector
|
||||||
|
// '~' general sibling combinator
|
||||||
|
// '*' wildcard
|
||||||
|
// ' ' descendant combinator
|
||||||
|
// Single-pass scan via find_first_of instead of eight sequential find() calls.
|
||||||
|
constexpr std::string_view kUnsupportedSelectorChars = "+>[:#~* ";
|
||||||
|
if (sel.find_first_of(kUnsupportedSelectorChars) != std::string_view::npos) return;
|
||||||
|
|
||||||
|
// Skip if this would exceed the rule limit
|
||||||
|
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||||
|
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
|
||||||
|
limitReached = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store or merge with existing. Hash/equal are case-insensitive, so two
|
||||||
|
// selectors that differ only in ASCII case collide on insert and merge.
|
||||||
|
auto it = rulesBySelector_.find(sel);
|
||||||
|
if (it != rulesBySelector_.end()) {
|
||||||
|
it->second.applyOver(style);
|
||||||
|
} else {
|
||||||
|
rulesBySelector_.emplace(std::string(sel), style);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main parsing entry point
|
||||||
|
|
||||||
|
bool CssParser::loadFromStream(HalFile& source) {
|
||||||
|
if (!source) {
|
||||||
|
LOG_ERR("CSS", "Cannot read from invalid file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t totalRead = 0;
|
||||||
|
|
||||||
|
// Use stack-allocated buffers for parsing to avoid heap reallocations
|
||||||
|
StackBuffer selector;
|
||||||
|
StackBuffer declBuffer;
|
||||||
|
|
||||||
|
bool inComment = false;
|
||||||
|
bool maybeSlash = false;
|
||||||
|
bool prevStar = false;
|
||||||
|
|
||||||
|
bool inAtRule = false;
|
||||||
|
int atDepth = 0;
|
||||||
|
|
||||||
|
int bodyDepth = 0;
|
||||||
|
bool skippingRule = false;
|
||||||
|
CssStyle currentStyle;
|
||||||
|
|
||||||
|
auto handleChar = [&](const char c) {
|
||||||
|
if (inAtRule) {
|
||||||
|
if (c == '{') {
|
||||||
|
++atDepth;
|
||||||
|
} else if (c == '}') {
|
||||||
|
if (atDepth > 0) --atDepth;
|
||||||
|
if (atDepth == 0) inAtRule = false;
|
||||||
|
} else if (c == ';' && atDepth == 0) {
|
||||||
|
inAtRule = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyDepth == 0) {
|
||||||
|
if (selector.empty() && isCssWhitespace(c)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c == '@' && selector.empty()) {
|
||||||
|
inAtRule = true;
|
||||||
|
atDepth = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c == '{') {
|
||||||
|
bodyDepth = 1;
|
||||||
|
currentStyle = CssStyle{};
|
||||||
|
declBuffer.clear();
|
||||||
|
if (selector.size() > MAX_SELECTOR_LENGTH * 4) {
|
||||||
|
skippingRule = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selector.push_back(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bodyDepth > 0
|
||||||
|
if (c == '{') {
|
||||||
|
++bodyDepth;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c == '}') {
|
||||||
|
--bodyDepth;
|
||||||
|
if (bodyDepth == 0) {
|
||||||
|
if (!skippingRule && !declBuffer.empty()) {
|
||||||
|
parseDeclarationIntoStyle(declBuffer, currentStyle);
|
||||||
|
}
|
||||||
|
if (!skippingRule) {
|
||||||
|
processRuleBlockWithStyle(selector, currentStyle);
|
||||||
|
}
|
||||||
|
selector.clear();
|
||||||
|
declBuffer.clear();
|
||||||
|
skippingRule = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bodyDepth > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!skippingRule) {
|
||||||
|
if (c == ';') {
|
||||||
|
if (!declBuffer.empty()) {
|
||||||
|
parseDeclarationIntoStyle(declBuffer, currentStyle);
|
||||||
|
declBuffer.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
declBuffer.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
char buffer[READ_BUFFER_SIZE];
|
||||||
|
while (source.available()) {
|
||||||
|
int bytesRead = source.read(buffer, sizeof(buffer));
|
||||||
|
if (bytesRead <= 0) break;
|
||||||
|
|
||||||
|
totalRead += static_cast<size_t>(bytesRead);
|
||||||
|
|
||||||
|
for (int i = 0; i < bytesRead; ++i) {
|
||||||
|
const char c = buffer[i];
|
||||||
|
|
||||||
|
if (inComment) {
|
||||||
|
if (prevStar && c == '/') {
|
||||||
|
inComment = false;
|
||||||
|
prevStar = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prevStar = c == '*';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybeSlash) {
|
||||||
|
if (c == '*') {
|
||||||
|
inComment = true;
|
||||||
|
maybeSlash = false;
|
||||||
|
prevStar = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
handleChar('/');
|
||||||
|
maybeSlash = false;
|
||||||
|
// fall through to process current char
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '/') {
|
||||||
|
maybeSlash = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChar(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybeSlash) {
|
||||||
|
handleChar('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CSS", "Parsed %zu rules from %zu bytes", rulesBySelector_.size(), totalRead);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style resolution
|
||||||
|
|
||||||
|
CssStyle CssParser::resolveStyle(std::string_view tagName, std::string_view classAttr) const {
|
||||||
|
static bool lowHeapWarningLogged = false;
|
||||||
|
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
|
||||||
|
if (!lowHeapWarningLogged) {
|
||||||
|
lowHeapWarningLogged = true;
|
||||||
|
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
|
||||||
|
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
|
||||||
|
}
|
||||||
|
return CssStyle{};
|
||||||
|
}
|
||||||
|
|
||||||
|
CssStyle result;
|
||||||
|
|
||||||
|
// 1. Apply element-level style (lowest priority). The map's hash/equal are
|
||||||
|
// case-insensitive, so the raw tagName view can be used as the lookup key.
|
||||||
|
if (auto it = rulesBySelector_.find(tagName); it != rulesBySelector_.end()) {
|
||||||
|
result.applyOver(it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classAttr.empty()) return result;
|
||||||
|
|
||||||
|
// TODO: Support combinations of classes (e.g. style on .class1.class2)
|
||||||
|
// 2. Apply class styles (medium priority). The transparent hash/equal accept
|
||||||
|
// a CompositeKey, so we never materialize the concatenation.
|
||||||
|
forEachDelimitedToken(classAttr, isCssWhitespace, [&](std::string_view cls) {
|
||||||
|
if (auto it = rulesBySelector_.find(CompositeKey{".", cls}); it != rulesBySelector_.end()) {
|
||||||
|
result.applyOver(it->second);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Support combinations of classes (e.g. style on p.class1.class2)
|
||||||
|
// 3. Apply element.class styles (higher priority).
|
||||||
|
forEachDelimitedToken(classAttr, isCssWhitespace, [&](std::string_view cls) {
|
||||||
|
if (auto it = rulesBySelector_.find(CompositeKey{tagName, ".", cls}); it != rulesBySelector_.end()) {
|
||||||
|
result.applyOver(it->second);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline style parsing (static - doesn't need rule database)
|
||||||
|
|
||||||
|
CssStyle CssParser::parseInlineStyle(std::string_view styleValue) { return parseDeclarations(styleValue); }
|
||||||
|
|
||||||
|
// Cache serialization
|
||||||
|
|
||||||
|
// Cache file name (version is CssParser::CSS_CACHE_VERSION)
|
||||||
|
constexpr char rulesCache[] = "/css_rules.cache";
|
||||||
|
|
||||||
|
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
||||||
|
|
||||||
|
void CssParser::deleteCache() const {
|
||||||
|
if (hasCache()) Storage.remove((cachePath + rulesCache).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::saveToCache() const {
|
||||||
|
if (cachePath.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile file;
|
||||||
|
if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write version
|
||||||
|
file.write(CssParser::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));
|
||||||
|
file.write(static_cast<uint8_t>(style.direction));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
writeLength(style.imageHeight);
|
||||||
|
writeLength(style.imageWidth);
|
||||||
|
file.write(static_cast<uint8_t>(style.display));
|
||||||
|
file.write(static_cast<uint8_t>(style.verticalAlign));
|
||||||
|
|
||||||
|
// Write defined flags as uint32_t
|
||||||
|
uint32_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;
|
||||||
|
if (style.defined.imageHeight) definedBits |= 1 << 13;
|
||||||
|
if (style.defined.imageWidth) definedBits |= 1 << 14;
|
||||||
|
if (style.defined.display) definedBits |= 1 << 15;
|
||||||
|
if (style.defined.direction) definedBits |= 1 << 16;
|
||||||
|
if (style.defined.verticalAlign) definedBits |= 1 << 17;
|
||||||
|
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CSS", "Saved %u rules to cache", ruleCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CssParser::loadFromCache() {
|
||||||
|
if (cachePath.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalFile file;
|
||||||
|
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing rules
|
||||||
|
clear();
|
||||||
|
|
||||||
|
// Read and verify version
|
||||||
|
uint8_t version = 0;
|
||||||
|
if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) {
|
||||||
|
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u), removing stale cache for rebuild", version,
|
||||||
|
CssParser::CSS_CACHE_VERSION);
|
||||||
|
// Explicitly close() file before calling Storage.remove()
|
||||||
|
file.close();
|
||||||
|
Storage.remove((cachePath + rulesCache).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read rule count
|
||||||
|
uint16_t ruleCount = 0;
|
||||||
|
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ruleCount > MAX_RULES) {
|
||||||
|
LOG_DBG("CSS", "Invalid cache rule count (%u > %zu)", ruleCount, MAX_RULES);
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size the bucket array up front to avoid incremental rehashes while loading rules.
|
||||||
|
rulesBySelector_.reserve(ruleCount);
|
||||||
|
|
||||||
|
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
||||||
|
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr size_t CSS_LENGTH_FIELD_COUNT = 11;
|
||||||
|
constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t);
|
||||||
|
constexpr size_t CSS_FIXED_STYLE_BYTES =
|
||||||
|
5 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + sizeof(uint8_t) + sizeof(uint32_t);
|
||||||
|
|
||||||
|
// Read each rule
|
||||||
|
for (uint16_t i = 0; i < ruleCount; ++i) {
|
||||||
|
// Read selector string
|
||||||
|
uint16_t selectorLen = 0;
|
||||||
|
if (!hasRemainingBytes(sizeof(selectorLen))) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || !hasRemainingBytes(selectorLen)) {
|
||||||
|
LOG_DBG("CSS", "Invalid selector length in cache: %u", selectorLen);
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string selector;
|
||||||
|
selector.resize(selectorLen);
|
||||||
|
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRemainingBytes(CSS_FIXED_STYLE_BYTES)) {
|
||||||
|
LOG_DBG("CSS", "Truncated CSS cache while reading style payload");
|
||||||
|
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 & CSS_TEXT_DECORATION_MASK);
|
||||||
|
|
||||||
|
if (file.read(&enumVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.direction = static_cast<CssTextDirection>(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) ||
|
||||||
|
!readLength(style.imageHeight) || !readLength(style.imageWidth)) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read display value
|
||||||
|
uint8_t displayVal;
|
||||||
|
if (file.read(&displayVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.display = static_cast<CssDisplay>(displayVal);
|
||||||
|
|
||||||
|
// Read verticalAlign value
|
||||||
|
uint8_t verticalAlignVal;
|
||||||
|
if (file.read(&verticalAlignVal, 1) != 1) {
|
||||||
|
rulesBySelector_.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style.verticalAlign = static_cast<CssVerticalAlign>(verticalAlignVal);
|
||||||
|
|
||||||
|
// Read defined flags
|
||||||
|
uint32_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;
|
||||||
|
style.defined.imageHeight = (definedBits & 1 << 13) != 0;
|
||||||
|
style.defined.imageWidth = (definedBits & 1 << 14) != 0;
|
||||||
|
style.defined.display = (definedBits & 1 << 15) != 0;
|
||||||
|
style.defined.direction = (definedBits & 1 << 16) != 0;
|
||||||
|
style.defined.verticalAlign = (definedBits & 1 << 17) != 0;
|
||||||
|
|
||||||
|
rulesBySelector_[selector] = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CssStyle.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight CSS parser for EPUB stylesheets
|
||||||
|
*
|
||||||
|
* Parses CSS files and extracts styling information relevant for e-ink display.
|
||||||
|
* Uses a two-phase approach: first tokenizes the CSS content, then builds
|
||||||
|
* a rule database that can be queried during HTML parsing.
|
||||||
|
*
|
||||||
|
* Supported selectors:
|
||||||
|
* - Element selectors: p, div, h1, etc.
|
||||||
|
* - Class selectors: .classname
|
||||||
|
* - Combined: element.classname
|
||||||
|
* - Grouped: selector1, selector2 { }
|
||||||
|
*
|
||||||
|
* Not supported (silently ignored):
|
||||||
|
* - Descendant/child selectors
|
||||||
|
* - Pseudo-classes and pseudo-elements
|
||||||
|
* - Media queries (content is skipped)
|
||||||
|
* - @import, @font-face, etc.
|
||||||
|
*/
|
||||||
|
class CssParser {
|
||||||
|
public:
|
||||||
|
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
||||||
|
static constexpr uint8_t CSS_CACHE_VERSION = 8;
|
||||||
|
|
||||||
|
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||||
|
~CssParser() = default;
|
||||||
|
|
||||||
|
// Non-copyable
|
||||||
|
CssParser(const CssParser&) = delete;
|
||||||
|
CssParser& operator=(const CssParser&) = delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and parse CSS from a file stream.
|
||||||
|
* Can be called multiple times to accumulate rules from multiple stylesheets.
|
||||||
|
* @param source Open file handle to read from
|
||||||
|
* @return true if parsing completed (even if no rules found)
|
||||||
|
*/
|
||||||
|
bool loadFromStream(HalFile& 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(std::string_view tagName, std::string_view classAttr) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an inline style attribute string.
|
||||||
|
* @param styleValue The value of a style="" attribute
|
||||||
|
* @return Parsed style properties
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any rules have been loaded
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool empty() const { return rulesBySelector_.empty(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of loaded rule sets
|
||||||
|
*/
|
||||||
|
[[nodiscard]] size_t ruleCount() const { return rulesBySelector_.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all loaded rules
|
||||||
|
*/
|
||||||
|
void clear() { rulesBySelector_.clear(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CSS rules cache file exists
|
||||||
|
*/
|
||||||
|
bool hasCache() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete CSS rules cache file exists
|
||||||
|
*/
|
||||||
|
void deleteCache() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save parsed CSS rules to a cache file.
|
||||||
|
* @return true if cache was written successfully
|
||||||
|
*/
|
||||||
|
bool saveToCache() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load CSS rules from a cache file.
|
||||||
|
* Clears any existing rules before loading.
|
||||||
|
* @return true if cache was loaded successfully
|
||||||
|
*/
|
||||||
|
bool loadFromCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Lookup key for a multi-piece selector. The pieces are hashed and compared
|
||||||
|
// as if concatenated, so callers can look up composite keys without
|
||||||
|
// materializing the concatenation in a scratch buffer. Constructed from a
|
||||||
|
// braced list of any arity, e.g. `CompositeKey{tagName, ".", cls}` or
|
||||||
|
// `CompositeKey{".", cls}`. The initializer_list's backing array lives for
|
||||||
|
// the full expression, which covers the lifetime of the find() call.
|
||||||
|
struct CompositeKey {
|
||||||
|
std::initializer_list<std::string_view> pieces;
|
||||||
|
CompositeKey(std::initializer_list<std::string_view> p) noexcept : pieces(p) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ASCII-case-insensitive transparent hash/equal. Stored selectors and lookup
|
||||||
|
// keys are compared without regard to case, so callers may insert and look up
|
||||||
|
// using whatever case the CSS source or HTML element name happens to use.
|
||||||
|
// Bodies live in CssParser.cpp so they can share the file-local asciiToLower.
|
||||||
|
struct SvHash {
|
||||||
|
using is_transparent = void;
|
||||||
|
size_t operator()(std::string_view sv) const noexcept;
|
||||||
|
size_t operator()(const std::string& s) const noexcept;
|
||||||
|
size_t operator()(CompositeKey k) const noexcept;
|
||||||
|
};
|
||||||
|
struct SvEqual {
|
||||||
|
using is_transparent = void;
|
||||||
|
bool operator()(std::string_view a, std::string_view b) const noexcept;
|
||||||
|
bool operator()(const std::string& a, std::string_view b) const noexcept;
|
||||||
|
bool operator()(std::string_view a, const std::string& b) const noexcept;
|
||||||
|
bool operator()(const std::string& a, const std::string& b) const noexcept;
|
||||||
|
bool operator()(CompositeKey a, std::string_view b) const noexcept;
|
||||||
|
bool operator()(std::string_view a, CompositeKey b) const noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Storage: maps selector -> style properties. Hash/equal are case-insensitive.
|
||||||
|
std::unordered_map<std::string, CssStyle, SvHash, SvEqual> rulesBySelector_;
|
||||||
|
|
||||||
|
std::string cachePath;
|
||||||
|
|
||||||
|
// Internal parsing helpers
|
||||||
|
void processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style);
|
||||||
|
static CssStyle parseDeclarations(std::string_view declBlock);
|
||||||
|
static void parseDeclarationIntoStyle(std::string_view decl, CssStyle& style);
|
||||||
|
|
||||||
|
// Individual property value parsers
|
||||||
|
static CssTextAlign interpretAlignment(std::string_view val);
|
||||||
|
static CssFontStyle interpretFontStyle(std::string_view val);
|
||||||
|
static CssFontWeight interpretFontWeight(std::string_view val);
|
||||||
|
static CssTextDecoration interpretDecoration(std::string_view val);
|
||||||
|
static CssLength interpretLength(std::string_view val);
|
||||||
|
/** Returns true only when a numeric length was parsed (e.g. 2em, 50%). False for auto/inherit/initial. */
|
||||||
|
static bool tryInterpretLength(std::string_view val, CssLength& out);
|
||||||
|
};
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
#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 };
|
||||||
|
enum class CssTextDirection : uint8_t { Ltr = 0, Rtl = 1 };
|
||||||
|
|
||||||
|
// 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. Values are bit flags so CSS can combine multiple line decorations.
|
||||||
|
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
|
||||||
|
|
||||||
|
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
|
||||||
|
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
|
||||||
|
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
|
||||||
|
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
|
||||||
|
|
||||||
|
// Display options - only None and Block are relevant for e-ink rendering
|
||||||
|
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
||||||
|
|
||||||
|
// Vertical alignment options for inline elements (e.g. superscript/subscript)
|
||||||
|
enum class CssVerticalAlign : uint8_t { Baseline = 0, Super = 1, Sub = 2 };
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
uint16_t imageHeight : 1;
|
||||||
|
uint16_t imageWidth : 1;
|
||||||
|
uint16_t display : 1;
|
||||||
|
uint16_t direction : 1;
|
||||||
|
uint16_t verticalAlign : 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),
|
||||||
|
imageHeight(0),
|
||||||
|
imageWidth(0),
|
||||||
|
display(0),
|
||||||
|
direction(0),
|
||||||
|
verticalAlign(0) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool anySet() const {
|
||||||
|
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||||||
|
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight ||
|
||||||
|
imageWidth || display || direction || verticalAlign;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAll() {
|
||||||
|
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||||
|
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||||
|
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||||
|
imageHeight = imageWidth = display = direction = verticalAlign = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache serializes defined flags as uint32_t with bit indices 0..17.
|
||||||
|
static_assert(sizeof(CssPropertyFlags) <= sizeof(uint32_t),
|
||||||
|
"CssPropertyFlags exceeds 32 bits; update cache read/write in CssParser.cpp");
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
CssTextDirection direction = CssTextDirection::Ltr;
|
||||||
|
|
||||||
|
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
|
||||||
|
CssLength imageHeight; // Height for img (e.g. 2em) – width derived from aspect ratio when only height set
|
||||||
|
CssLength imageWidth; // Width for img when both or only width set
|
||||||
|
CssDisplay display = CssDisplay::Block; // display property (Block or None)
|
||||||
|
CssVerticalAlign verticalAlign = CssVerticalAlign::Baseline; // vertical-align (super/sub positioning)
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (base.hasImageHeight()) {
|
||||||
|
imageHeight = base.imageHeight;
|
||||||
|
defined.imageHeight = 1;
|
||||||
|
}
|
||||||
|
if (base.hasImageWidth()) {
|
||||||
|
imageWidth = base.imageWidth;
|
||||||
|
defined.imageWidth = 1;
|
||||||
|
}
|
||||||
|
if (base.hasDisplay()) {
|
||||||
|
display = base.display;
|
||||||
|
defined.display = 1;
|
||||||
|
}
|
||||||
|
if (base.hasDirection()) {
|
||||||
|
direction = base.direction;
|
||||||
|
defined.direction = 1;
|
||||||
|
}
|
||||||
|
if (base.hasVerticalAlign()) {
|
||||||
|
verticalAlign = base.verticalAlign;
|
||||||
|
defined.verticalAlign = 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; }
|
||||||
|
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
|
||||||
|
[[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; }
|
||||||
|
[[nodiscard]] bool hasDisplay() const { return defined.display; }
|
||||||
|
[[nodiscard]] bool hasDirection() const { return defined.direction; }
|
||||||
|
[[nodiscard]] bool hasVerticalAlign() const { return defined.verticalAlign; }
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
textAlign = CssTextAlign::Left;
|
||||||
|
fontStyle = CssFontStyle::Normal;
|
||||||
|
fontWeight = CssFontWeight::Normal;
|
||||||
|
textDecoration = CssTextDecoration::None;
|
||||||
|
direction = CssTextDirection::Ltr;
|
||||||
|
textIndent = CssLength{};
|
||||||
|
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||||
|
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||||
|
imageHeight = imageWidth = CssLength{};
|
||||||
|
display = CssDisplay::Block;
|
||||||
|
verticalAlign = CssVerticalAlign::Baseline;
|
||||||
|
defined.clearAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// based on
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#include "htmlEntities.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
struct EntityPair {
|
||||||
|
const char* key;
|
||||||
|
const char* value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sorted lexicographically by key to allow binary search.
|
||||||
|
static constexpr EntityPair ENTITY_LOOKUP[] = {
|
||||||
|
{"Æ", "Æ"}, {"Á", "Á"}, {"Â", "Â"}, {"À", "À"}, {"Α", "Α"},
|
||||||
|
{"Å", "Å"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Β", "Β"}, {"Ç", "Ç"},
|
||||||
|
{"Χ", "Χ"}, {"‡", "‡"}, {"Δ", "Δ"}, {"Ð", "Ð"}, {"É", "É"},
|
||||||
|
{"Ê", "Ê"}, {"È", "È"}, {"Ε", "Ε"}, {"Η", "Η"}, {"Ë", "Ë"},
|
||||||
|
{"Γ", "Γ"}, {"Í", "Í"}, {"Î", "Î"}, {"Ì", "Ì"}, {"Ι", "Ι"},
|
||||||
|
{"Ï", "Ï"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"}, {"Ñ", "Ñ"},
|
||||||
|
{"Ν", "Ν"}, {"Œ", "Œ"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Ò", "Ò"},
|
||||||
|
{"Ω", "Ω"}, {"Ο", "Ο"}, {"Ø", "Ø"}, {"Õ", "Õ"}, {"Ö", "Ö"},
|
||||||
|
{"Φ", "Φ"}, {"Π", "Π"}, {"″", "″"}, {"Ψ", "Ψ"}, {"Ρ", "Ρ"},
|
||||||
|
{"Š", "Š"}, {"Σ", "Σ"}, {"Þ", "Þ"}, {"Τ", "Τ"}, {"Θ", "Θ"},
|
||||||
|
{"Ú", "Ú"}, {"Û", "Û"}, {"Ù", "Ù"}, {"Υ", "Υ"}, {"Ü", "Ü"},
|
||||||
|
{"Ξ", "Ξ"}, {"Ý", "Ý"}, {"Ÿ", "Ÿ"}, {"Ζ", "Ζ"}, {"á", "á"},
|
||||||
|
{"â", "â"}, {"´", "´"}, {"æ", "æ"}, {"à", "à"}, {"ℵ", "ℵ"},
|
||||||
|
{"α", "α"}, {"&", "&"}, {"∧", "∧"}, {"∠", "∠"}, {"å", "å"},
|
||||||
|
{"≈", "≈"}, {"ã", "ã"}, {"ä", "ä"}, {"„", "„"}, {"β", "β"},
|
||||||
|
{"¦", "¦"}, {"•", "•"}, {"∩", "∩"}, {"ç", "ç"}, {"¸", "¸"},
|
||||||
|
{"¢", "¢"}, {"χ", "χ"}, {"ˆ", "ˆ"}, {"♣", "♣"}, {"≅", "≅"},
|
||||||
|
{"©", "©"}, {"↵", "↵"}, {"∪", "∪"}, {"¤", "¤"}, {"⇓", "⇓"},
|
||||||
|
{"†", "†"}, {"↓", "↓"}, {"°", "°"}, {"δ", "δ"}, {"♦", "♦"},
|
||||||
|
{"÷", "÷"}, {"é", "é"}, {"ê", "ê"}, {"è", "è"}, {"∅", "∅"},
|
||||||
|
{" ", " "}, {" ", " "}, {"ε", "ε"}, {"≡", "≡"}, {"η", "η"},
|
||||||
|
{"ð", "ð"}, {"ë", "ë"}, {"€", "€"}, {"∃", "∃"}, {"ƒ", "ƒ"},
|
||||||
|
{"∀", "∀"}, {"½", "½"}, {"¼", "¼"}, {"¾", "¾"}, {"⁄", "⁄"},
|
||||||
|
{"γ", "γ"}, {"≥", "≥"}, {">", ">"}, {"⇔", "⇔"}, {"↔", "↔"},
|
||||||
|
{"♥", "♥"}, {"…", "…"}, {"í", "í"}, {"î", "î"}, {"¡", "¡"},
|
||||||
|
{"ì", "ì"}, {"ℑ", "ℑ"}, {"∞", "∞"}, {"∫", "∫"}, {"ι", "ι"},
|
||||||
|
{"¿", "¿"}, {"∈", "∈"}, {"ï", "ï"}, {"κ", "κ"}, {"⇐", "⇐"},
|
||||||
|
{"λ", "λ"}, {"⟨", "〈"}, {"«", "«"}, {"←", "←"}, {"⌈", "⌈"},
|
||||||
|
{"“", "\u201C"}, {"≤", "≤"}, {"⌊", "⌊"}, {"∗", "∗"}, {"◊", "◊"},
|
||||||
|
{"‎", "\u200E"}, {"‹", "‹"}, {"‘", "\u2018"}, {"<", "<"}, {"¯", "¯"},
|
||||||
|
{"—", "—"}, {"µ", "µ"}, {"·", "·"}, {"−", "−"}, {"μ", "μ"},
|
||||||
|
{"∇", "∇"}, {" ", "\xC2\xA0"}, {"–", "–"}, {"≠", "≠"}, {"∋", "∋"},
|
||||||
|
{"¬", "¬"}, {"∉", "∉"}, {"⊄", "⊄"}, {"ñ", "ñ"}, {"ν", "ν"},
|
||||||
|
{"ó", "ó"}, {"ô", "ô"}, {"œ", "œ"}, {"ò", "ò"}, {"‾", "‾"},
|
||||||
|
{"ω", "ω"}, {"ο", "ο"}, {"⊕", "⊕"}, {"∨", "∨"}, {"ª", "ª"},
|
||||||
|
{"º", "º"}, {"ø", "ø"}, {"õ", "õ"}, {"⊗", "⊗"}, {"ö", "ö"},
|
||||||
|
{"¶", "¶"}, {"∂", "∂"}, {"‰", "‰"}, {"⊥", "⊥"}, {"φ", "φ"},
|
||||||
|
{"π", "π"}, {"ϖ", "ϖ"}, {"±", "±"}, {"£", "£"}, {"′", "′"},
|
||||||
|
{"∏", "∏"}, {"∝", "∝"}, {"ψ", "ψ"}, {""", "\""}, {"⇒", "⇒"},
|
||||||
|
{"√", "√"}, {"⟩", "〉"}, {"»", "»"}, {"→", "→"}, {"⌉", "⌉"},
|
||||||
|
{"”", "\u201D"}, {"ℜ", "\u211C"}, {"®", "®"}, {"⌋", "⌋"}, {"ρ", "ρ"},
|
||||||
|
{"‏", "\u200F"}, {"›", "›"}, {"’", "\u2019"}, {"‚", "‚"}, {"š", "š"},
|
||||||
|
{"⋅", "⋅"}, {"§", "§"}, {"­", "\xC2\xAD"}, {"σ", "σ"}, {"ς", "ς"},
|
||||||
|
{"∼", "∼"}, {"♠", "♠"}, {"⊂", "⊂"}, {"⊆", "⊆"}, {"∑", "∑"},
|
||||||
|
{"¹", "¹"}, {"²", "²"}, {"³", "³"}, {"⊃", "⊃"}, {"⊇", "⊇"},
|
||||||
|
{"ß", "ß"}, {"τ", "τ"}, {"∴", "∴"}, {"θ", "θ"}, {"ϑ", "ϑ"},
|
||||||
|
{" ", " "}, {"þ", "þ"}, {"˜", "˜"}, {"×", "×"}, {"™", "™"},
|
||||||
|
{"⇑", "⇑"}, {"ú", "ú"}, {"↑", "↑"}, {"û", "û"}, {"ù", "ù"},
|
||||||
|
{"¨", "¨"}, {"ϒ", "ϒ"}, {"υ", "υ"}, {"ü", "ü"}, {"℘", "℘"},
|
||||||
|
{"ξ", "ξ"}, {"ý", "ý"}, {"¥", "¥"}, {"ÿ", "ÿ"}, {"ζ", "ζ"},
|
||||||
|
{"‍", "\u200D"}, {"‌", "\u200C"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the table is sorted at compile time.
|
||||||
|
static constexpr int constexprStrcmp(const char* a, const char* b) {
|
||||||
|
for (size_t i = 0;; i++) {
|
||||||
|
if (a[i] != b[i]) return (unsigned char)a[i] < (unsigned char)b[i] ? -1 : 1;
|
||||||
|
if (a[i] == '\0') return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr bool isTableSorted() {
|
||||||
|
for (size_t i = 1; i < std::size(ENTITY_LOOKUP); i++) {
|
||||||
|
if (constexprStrcmp(ENTITY_LOOKUP[i - 1].key, ENTITY_LOOKUP[i].key) >= 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(isTableSorted(), "ENTITY_LOOKUP must be sorted lexicographically by key");
|
||||||
|
|
||||||
|
// Lookup a single HTML entity and return its UTF-8 value.
|
||||||
|
const char* lookupHtmlEntity(const char* entity, size_t len) {
|
||||||
|
if (entity == nullptr || len == 0) return nullptr;
|
||||||
|
|
||||||
|
size_t lo = 0;
|
||||||
|
size_t hi = std::size(ENTITY_LOOKUP);
|
||||||
|
|
||||||
|
while (lo < hi) {
|
||||||
|
const size_t mid = lo + (hi - lo) / 2;
|
||||||
|
const char* key = ENTITY_LOOKUP[mid].key;
|
||||||
|
const size_t keyLen = strlen(key);
|
||||||
|
const size_t cmpLen = (len < keyLen) ? len : keyLen;
|
||||||
|
int cmp = memcmp(entity, key, cmpLen);
|
||||||
|
if (cmp == 0) {
|
||||||
|
// safety net: if prefix equal, shorter string is considered smaller
|
||||||
|
if (len < keyLen)
|
||||||
|
cmp = -1;
|
||||||
|
else if (len > keyLen)
|
||||||
|
cmp = 1;
|
||||||
|
else
|
||||||
|
cmp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmp == 0) return ENTITY_LOOKUP[mid].value;
|
||||||
|
if (cmp < 0)
|
||||||
|
hi = mid;
|
||||||
|
else
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// based on
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Lookup a single HTML entity (including & and ;) and return its UTF-8 value
|
||||||
|
// Returns nullptr if entity is not found
|
||||||
|
const char* lookupHtmlEntity(const char* entity, size_t len);
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
#include "HyphenationCommon.h"
|
||||||
|
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Convert Latin uppercase letters (ASCII plus Latin-1 supplement) to lowercase
|
||||||
|
uint32_t toLowerLatinImpl(const uint32_t cp) {
|
||||||
|
if (cp >= 'A' && cp <= 'Z') {
|
||||||
|
return cp - 'A' + 'a';
|
||||||
|
}
|
||||||
|
if ((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00DE)) {
|
||||||
|
return cp + 0x20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latin Extended-A (U+0100..U+017E): uppercase letters are paired with
|
||||||
|
// lowercase at cp+1. Two sub-ranges have different alignment:
|
||||||
|
// U+0100..U+0137: uppercase on EVEN codepoints
|
||||||
|
// U+0139..U+0148: uppercase on ODD codepoints
|
||||||
|
// U+014A..U+0177: uppercase on EVEN codepoints
|
||||||
|
// U+0179..U+017E: uppercase on ODD codepoints
|
||||||
|
// Covers Polish (Ą/ą, Ć/ć, Ę/ę, Ł/ł, Ń/ń, Ś/ś, Ź/ź, Ż/ż), Czech, Hungarian, Turkish, etc.
|
||||||
|
if ((cp >= 0x0100 && cp <= 0x0137 && (cp % 2 == 0)) || (cp >= 0x0139 && cp <= 0x0148 && (cp % 2 == 1)) ||
|
||||||
|
(cp >= 0x014A && cp <= 0x0177 && (cp % 2 == 0)) || (cp >= 0x0179 && cp <= 0x017E && (cp % 2 == 1))) {
|
||||||
|
return cp + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cp) {
|
||||||
|
case 0x0178: // Ÿ
|
||||||
|
return 0x00FF; // ÿ
|
||||||
|
case 0x1E9E: // ẞ
|
||||||
|
return 0x00DF; // ß
|
||||||
|
default:
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Cyrillic uppercase letters to lowercase
|
||||||
|
// Cyrillic uppercase range 0x0410-0x042F maps to lowercase by adding 0x20
|
||||||
|
// Special case: Cyrillic capital IO (0x0401) maps to lowercase io (0x0451)
|
||||||
|
uint32_t toLowerCyrillicImpl(const uint32_t cp) {
|
||||||
|
if (cp >= 0x0410 && cp <= 0x042F) {
|
||||||
|
return cp + 0x20;
|
||||||
|
}
|
||||||
|
if (cp == 0x0401) {
|
||||||
|
return 0x0451;
|
||||||
|
}
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
uint32_t toLowerLatin(const uint32_t cp) { return toLowerLatinImpl(cp); }
|
||||||
|
|
||||||
|
uint32_t toLowerCyrillic(const uint32_t cp) { return toLowerCyrillicImpl(cp); }
|
||||||
|
|
||||||
|
bool isLatinLetter(const uint32_t cp) {
|
||||||
|
if ((cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00F6) || (cp >= 0x00F8 && cp <= 0x00FF)) &&
|
||||||
|
cp != 0x00D7 && cp != 0x00F7) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latin Extended-A (U+0100..U+017F): Polish, Czech, Hungarian, Turkish, etc.
|
||||||
|
if (cp >= 0x0100 && cp <= 0x017F) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cp) {
|
||||||
|
case 0x0152: // Œ
|
||||||
|
case 0x0153: // œ
|
||||||
|
case 0x0178: // Ÿ
|
||||||
|
case 0x1E9E: // ẞ
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCyrillicLetter(const uint32_t cp) { return (cp >= 0x0400 && cp <= 0x052F); }
|
||||||
|
|
||||||
|
bool isAlphabetic(const uint32_t cp) { return isLatinLetter(cp) || isCyrillicLetter(cp); }
|
||||||
|
|
||||||
|
bool isPunctuation(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '-':
|
||||||
|
case '.':
|
||||||
|
case ',':
|
||||||
|
case '!':
|
||||||
|
case '?':
|
||||||
|
case ';':
|
||||||
|
case ':':
|
||||||
|
case '"':
|
||||||
|
case '\'':
|
||||||
|
case ')':
|
||||||
|
case '(':
|
||||||
|
case 0x00AB: // «
|
||||||
|
case 0x00BB: // »
|
||||||
|
case 0x2018: // ‘
|
||||||
|
case 0x2019: // ’
|
||||||
|
case 0x201A: // ‚
|
||||||
|
case 0x201C: // “
|
||||||
|
case 0x201D: // ”
|
||||||
|
case 0x201E: // „
|
||||||
|
case 0x00A0: // no-break space
|
||||||
|
case '{':
|
||||||
|
case '}':
|
||||||
|
case '[':
|
||||||
|
case ']':
|
||||||
|
case '/':
|
||||||
|
case 0x2039: // ‹
|
||||||
|
case 0x203A: // ›
|
||||||
|
case 0x2026: // …
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
|
||||||
|
|
||||||
|
bool isApostrophe(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '\'':
|
||||||
|
case 0x2018: // left single quotation mark
|
||||||
|
case 0x2019: // right single quotation mark
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isExplicitHyphen(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '-':
|
||||||
|
case 0x00AD: // soft hyphen
|
||||||
|
case 0x058A: // Armenian hyphen
|
||||||
|
case 0x2010: // hyphen
|
||||||
|
case 0x2011: // non-breaking hyphen
|
||||||
|
case 0x2012: // figure dash
|
||||||
|
case 0x2013: // en dash
|
||||||
|
case 0x2014: // em dash
|
||||||
|
case 0x2015: // horizontal bar
|
||||||
|
case 0x2043: // hyphen bullet
|
||||||
|
case 0x207B: // superscript minus
|
||||||
|
case 0x208B: // subscript minus
|
||||||
|
case 0x2212: // minus sign
|
||||||
|
case 0x2E17: // double oblique hyphen
|
||||||
|
case 0x2E3A: // two-em dash
|
||||||
|
case 0x2E3B: // three-em dash
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSoftHyphen(const uint32_t cp) { return cp == 0x00AD; }
|
||||||
|
|
||||||
|
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps) {
|
||||||
|
if (cps.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing footnote references like [12], even if punctuation trails after the closing bracket.
|
||||||
|
if (cps.size() >= 3) {
|
||||||
|
int end = static_cast<int>(cps.size()) - 1;
|
||||||
|
while (end >= 0 && isPunctuation(cps[end].value)) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
int pos = end;
|
||||||
|
if (pos >= 0 && isAsciiDigit(cps[pos].value)) {
|
||||||
|
while (pos >= 0 && isAsciiDigit(cps[pos].value)) {
|
||||||
|
--pos;
|
||||||
|
}
|
||||||
|
if (pos >= 0 && cps[pos].value == '[' && end - pos > 1) {
|
||||||
|
cps.erase(cps.begin() + pos, cps.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!cps.empty() && isPunctuation(cps.front().value)) {
|
||||||
|
cps.erase(cps.begin());
|
||||||
|
}
|
||||||
|
while (!cps.empty() && isPunctuation(cps.back().value)) {
|
||||||
|
cps.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
|
||||||
|
std::vector<CodepointInfo> cps;
|
||||||
|
cps.reserve(word.size());
|
||||||
|
|
||||||
|
const unsigned char* base = reinterpret_cast<const unsigned char*>(word.c_str());
|
||||||
|
const unsigned char* ptr = base;
|
||||||
|
while (*ptr != 0) {
|
||||||
|
const unsigned char* current = ptr;
|
||||||
|
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||||
|
// If this is a combining diacritic (e.g., U+0301 = acute) and there's
|
||||||
|
// a previous base character that can be composed into a single
|
||||||
|
// precomposed Unicode scalar (Latin-1 / Latin-Extended), do that
|
||||||
|
// composition here. This provides lightweight NFC-like behavior for
|
||||||
|
// common Western European diacritics (acute, grave, circumflex, tilde,
|
||||||
|
// diaeresis, cedilla) without pulling in a full Unicode normalization
|
||||||
|
// library.
|
||||||
|
if (!cps.empty()) {
|
||||||
|
uint32_t prev = cps.back().value;
|
||||||
|
uint32_t composed = 0;
|
||||||
|
switch (cp) {
|
||||||
|
case 0x0300: // grave
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0041:
|
||||||
|
composed = 0x00C0;
|
||||||
|
break; // A -> À
|
||||||
|
case 0x0061:
|
||||||
|
composed = 0x00E0;
|
||||||
|
break; // a -> à
|
||||||
|
case 0x0045:
|
||||||
|
composed = 0x00C8;
|
||||||
|
break; // E -> È
|
||||||
|
case 0x0065:
|
||||||
|
composed = 0x00E8;
|
||||||
|
break; // e -> è
|
||||||
|
case 0x0049:
|
||||||
|
composed = 0x00CC;
|
||||||
|
break; // I -> Ì
|
||||||
|
case 0x0069:
|
||||||
|
composed = 0x00EC;
|
||||||
|
break; // i -> ì
|
||||||
|
case 0x004F:
|
||||||
|
composed = 0x00D2;
|
||||||
|
break; // O -> Ò
|
||||||
|
case 0x006F:
|
||||||
|
composed = 0x00F2;
|
||||||
|
break; // o -> ò
|
||||||
|
case 0x0055:
|
||||||
|
composed = 0x00D9;
|
||||||
|
break; // U -> Ù
|
||||||
|
case 0x0075:
|
||||||
|
composed = 0x00F9;
|
||||||
|
break; // u -> ù
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0301: // acute
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0041:
|
||||||
|
composed = 0x00C1;
|
||||||
|
break; // A -> Á
|
||||||
|
case 0x0061:
|
||||||
|
composed = 0x00E1;
|
||||||
|
break; // a -> á
|
||||||
|
case 0x0045:
|
||||||
|
composed = 0x00C9;
|
||||||
|
break; // E -> É
|
||||||
|
case 0x0065:
|
||||||
|
composed = 0x00E9;
|
||||||
|
break; // e -> é
|
||||||
|
case 0x0049:
|
||||||
|
composed = 0x00CD;
|
||||||
|
break; // I -> Í
|
||||||
|
case 0x0069:
|
||||||
|
composed = 0x00ED;
|
||||||
|
break; // i -> í
|
||||||
|
case 0x004F:
|
||||||
|
composed = 0x00D3;
|
||||||
|
break; // O -> Ó
|
||||||
|
case 0x006F:
|
||||||
|
composed = 0x00F3;
|
||||||
|
break; // o -> ó
|
||||||
|
case 0x0055:
|
||||||
|
composed = 0x00DA;
|
||||||
|
break; // U -> Ú
|
||||||
|
case 0x0075:
|
||||||
|
composed = 0x00FA;
|
||||||
|
break; // u -> ú
|
||||||
|
case 0x0059:
|
||||||
|
composed = 0x00DD;
|
||||||
|
break; // Y -> Ý
|
||||||
|
case 0x0079:
|
||||||
|
composed = 0x00FD;
|
||||||
|
break; // y -> ý
|
||||||
|
case 0x0043:
|
||||||
|
composed = 0x0106;
|
||||||
|
break; // C -> Ć
|
||||||
|
case 0x0063:
|
||||||
|
composed = 0x0107;
|
||||||
|
break; // c -> ć
|
||||||
|
case 0x004E:
|
||||||
|
composed = 0x0143;
|
||||||
|
break; // N -> Ń
|
||||||
|
case 0x006E:
|
||||||
|
composed = 0x0144;
|
||||||
|
break; // n -> ń
|
||||||
|
case 0x0053:
|
||||||
|
composed = 0x015A;
|
||||||
|
break; // S -> Ś
|
||||||
|
case 0x0073:
|
||||||
|
composed = 0x015B;
|
||||||
|
break; // s -> ś
|
||||||
|
case 0x005A:
|
||||||
|
composed = 0x0179;
|
||||||
|
break; // Z -> Ź
|
||||||
|
case 0x007A:
|
||||||
|
composed = 0x017A;
|
||||||
|
break; // z -> ź
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0302: // circumflex
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0041:
|
||||||
|
composed = 0x00C2;
|
||||||
|
break; // A -> Â
|
||||||
|
case 0x0061:
|
||||||
|
composed = 0x00E2;
|
||||||
|
break; // a -> â
|
||||||
|
case 0x0045:
|
||||||
|
composed = 0x00CA;
|
||||||
|
break; // E -> Ê
|
||||||
|
case 0x0065:
|
||||||
|
composed = 0x00EA;
|
||||||
|
break; // e -> ê
|
||||||
|
case 0x0049:
|
||||||
|
composed = 0x00CE;
|
||||||
|
break; // I -> Î
|
||||||
|
case 0x0069:
|
||||||
|
composed = 0x00EE;
|
||||||
|
break; // i -> î
|
||||||
|
case 0x004F:
|
||||||
|
composed = 0x00D4;
|
||||||
|
break; // O -> Ô
|
||||||
|
case 0x006F:
|
||||||
|
composed = 0x00F4;
|
||||||
|
break; // o -> ô
|
||||||
|
case 0x0055:
|
||||||
|
composed = 0x00DB;
|
||||||
|
break; // U -> Û
|
||||||
|
case 0x0075:
|
||||||
|
composed = 0x00FB;
|
||||||
|
break; // u -> û
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0303: // tilde
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0041:
|
||||||
|
composed = 0x00C3;
|
||||||
|
break; // A -> Ã
|
||||||
|
case 0x0061:
|
||||||
|
composed = 0x00E3;
|
||||||
|
break; // a -> ã
|
||||||
|
case 0x004E:
|
||||||
|
composed = 0x00D1;
|
||||||
|
break; // N -> Ñ
|
||||||
|
case 0x006E:
|
||||||
|
composed = 0x00F1;
|
||||||
|
break; // n -> ñ
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0308: // diaeresis/umlaut
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0041:
|
||||||
|
composed = 0x00C4;
|
||||||
|
break; // A -> Ä
|
||||||
|
case 0x0061:
|
||||||
|
composed = 0x00E4;
|
||||||
|
break; // a -> ä
|
||||||
|
case 0x0045:
|
||||||
|
composed = 0x00CB;
|
||||||
|
break; // E -> Ë
|
||||||
|
case 0x0065:
|
||||||
|
composed = 0x00EB;
|
||||||
|
break; // e -> ë
|
||||||
|
case 0x0049:
|
||||||
|
composed = 0x00CF;
|
||||||
|
break; // I -> Ï
|
||||||
|
case 0x0069:
|
||||||
|
composed = 0x00EF;
|
||||||
|
break; // i -> ï
|
||||||
|
case 0x004F:
|
||||||
|
composed = 0x00D6;
|
||||||
|
break; // O -> Ö
|
||||||
|
case 0x006F:
|
||||||
|
composed = 0x00F6;
|
||||||
|
break; // o -> ö
|
||||||
|
case 0x0055:
|
||||||
|
composed = 0x00DC;
|
||||||
|
break; // U -> Ü
|
||||||
|
case 0x0075:
|
||||||
|
composed = 0x00FC;
|
||||||
|
break; // u -> ü
|
||||||
|
case 0x0059:
|
||||||
|
composed = 0x0178;
|
||||||
|
break; // Y -> Ÿ
|
||||||
|
case 0x0079:
|
||||||
|
composed = 0x00FF;
|
||||||
|
break; // y -> ÿ
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0307: // dot above (Polish Ż)
|
||||||
|
switch (prev) {
|
||||||
|
case 0x005A:
|
||||||
|
composed = 0x017B;
|
||||||
|
break; // Z -> Ż
|
||||||
|
case 0x007A:
|
||||||
|
composed = 0x017C;
|
||||||
|
break; // z -> ż
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0327: // cedilla
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0043:
|
||||||
|
composed = 0x00C7;
|
||||||
|
break; // C -> Ç
|
||||||
|
case 0x0063:
|
||||||
|
composed = 0x00E7;
|
||||||
|
break; // c -> ç
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x0328: // ogonek (Polish Ą, Ę)
|
||||||
|
switch (prev) {
|
||||||
|
case 0x0041:
|
||||||
|
composed = 0x0104;
|
||||||
|
break; // A -> Ą
|
||||||
|
case 0x0061:
|
||||||
|
composed = 0x0105;
|
||||||
|
break; // a -> ą
|
||||||
|
case 0x0045:
|
||||||
|
composed = 0x0118;
|
||||||
|
break; // E -> Ę
|
||||||
|
case 0x0065:
|
||||||
|
composed = 0x0119;
|
||||||
|
break; // e -> ę
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (composed != 0) {
|
||||||
|
cps.back().value = composed;
|
||||||
|
continue; // skip pushing the combining mark itself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cps.push_back({cp, static_cast<size_t>(current - base)});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cps;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct CodepointInfo {
|
||||||
|
uint32_t value;
|
||||||
|
size_t byteOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t toLowerLatin(uint32_t cp);
|
||||||
|
uint32_t toLowerCyrillic(uint32_t cp);
|
||||||
|
|
||||||
|
bool isLatinLetter(uint32_t cp);
|
||||||
|
bool isCyrillicLetter(uint32_t cp);
|
||||||
|
|
||||||
|
bool isAlphabetic(uint32_t cp);
|
||||||
|
bool isPunctuation(uint32_t cp);
|
||||||
|
bool isAsciiDigit(uint32_t cp);
|
||||||
|
bool isApostrophe(uint32_t cp);
|
||||||
|
bool isExplicitHyphen(uint32_t cp);
|
||||||
|
bool isSoftHyphen(uint32_t cp);
|
||||||
|
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
|
||||||
|
std::vector<CodepointInfo> collectCodepoints(const std::string& word);
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
#include "Hyphenator.h"
|
||||||
|
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "HyphenationCommon.h"
|
||||||
|
#include "LanguageHyphenator.h"
|
||||||
|
#include "LanguageRegistry.h"
|
||||||
|
|
||||||
|
const LanguageHyphenator* Hyphenator::cachedHyphenator_ = nullptr;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Normalize ISO 639-2 (three-letter) codes to ISO 639-1 (two-letter) codes used by the
|
||||||
|
// hyphenation registry. EPUBs may use either form in their dc:language metadata (e.g.
|
||||||
|
// "eng" instead of "en"). Both the bibliographic ("fre"/"ger") and terminological
|
||||||
|
// ("fra"/"deu") ISO 639-2 variants are mapped.
|
||||||
|
struct Iso639Mapping {
|
||||||
|
const char* iso639_2;
|
||||||
|
const char* iso639_1;
|
||||||
|
};
|
||||||
|
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||||
|
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
||||||
|
{"ukr", "uk"}, {"swe", "sv"}, {"fin", "fi"}};
|
||||||
|
|
||||||
|
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||||
|
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||||
|
if (langTag.empty()) return nullptr;
|
||||||
|
|
||||||
|
// Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en", "ENG" -> "en").
|
||||||
|
std::string primary;
|
||||||
|
primary.reserve(langTag.size());
|
||||||
|
for (char c : langTag) {
|
||||||
|
if (c == '-' || c == '_') break;
|
||||||
|
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||||
|
primary.push_back(c);
|
||||||
|
}
|
||||||
|
if (primary.empty()) return nullptr;
|
||||||
|
|
||||||
|
// Normalize ISO 639-2 three-letter codes to two-letter equivalents.
|
||||||
|
for (const auto& mapping : kIso639Mappings) {
|
||||||
|
if (primary == mapping.iso639_2) {
|
||||||
|
primary = mapping.iso639_1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLanguageHyphenatorForPrimaryTag(primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps a codepoint index back to its byte offset inside the source word.
|
||||||
|
size_t byteOffsetForIndex(const std::vector<CodepointInfo>& cps, const size_t index) {
|
||||||
|
return (index < cps.size()) ? cps[index].byteOffset : (cps.empty() ? 0 : cps.back().byteOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a vector of break information from explicit hyphen markers in the given codepoints.
|
||||||
|
// Only hyphens that appear between two alphabetic characters are considered valid breaks.
|
||||||
|
//
|
||||||
|
// Example: "US-Satellitensystems" (cps: U, S, -, S, a, t, ...)
|
||||||
|
// -> finds '-' at index 2 with alphabetic neighbors 'S' and 'S'
|
||||||
|
// -> returns one BreakInfo at the byte offset of 'S' (the char after '-'),
|
||||||
|
// with requiresInsertedHyphen=false because '-' is already visible.
|
||||||
|
//
|
||||||
|
// Example: "Satel\u00ADliten" (soft-hyphen between 'l' and 'l')
|
||||||
|
// -> returns one BreakInfo with requiresInsertedHyphen=true (soft-hyphen
|
||||||
|
// is invisible and needs a visible '-' when the break is used).
|
||||||
|
std::vector<Hyphenator::BreakInfo> buildExplicitBreakInfos(const std::vector<CodepointInfo>& cps) {
|
||||||
|
std::vector<Hyphenator::BreakInfo> breaks;
|
||||||
|
|
||||||
|
for (size_t i = 1; i + 1 < cps.size(); ++i) {
|
||||||
|
const uint32_t cp = cps[i].value;
|
||||||
|
if (!isExplicitHyphen(cp) || !isAlphabetic(cps[i - 1].value) || !isAlphabetic(cps[i + 1].value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Offset points to the next codepoint so rendering starts after the hyphen marker.
|
||||||
|
breaks.push_back({cps[i + 1].byteOffset, isSoftHyphen(cp)});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSegmentSeparator(const uint32_t cp) { return isExplicitHyphen(cp) || isApostrophe(cp); }
|
||||||
|
|
||||||
|
void appendSegmentPatternBreaks(const std::vector<CodepointInfo>& cps, const LanguageHyphenator& hyphenator,
|
||||||
|
const bool includeFallback, std::vector<Hyphenator::BreakInfo>& outBreaks) {
|
||||||
|
size_t segStart = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i <= cps.size(); ++i) {
|
||||||
|
const bool atEnd = i == cps.size();
|
||||||
|
const bool atSeparator = !atEnd && isSegmentSeparator(cps[i].value);
|
||||||
|
if (!atEnd && !atSeparator) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > segStart) {
|
||||||
|
std::vector<CodepointInfo> segment(cps.begin() + segStart, cps.begin() + i);
|
||||||
|
auto segIndexes = hyphenator.breakIndexes(segment);
|
||||||
|
|
||||||
|
if (includeFallback && segIndexes.empty()) {
|
||||||
|
const size_t minPrefix = hyphenator.minPrefix();
|
||||||
|
const size_t minSuffix = hyphenator.minSuffix();
|
||||||
|
for (size_t idx = minPrefix; idx + minSuffix <= segment.size(); ++idx) {
|
||||||
|
segIndexes.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const size_t idx : segIndexes) {
|
||||||
|
assert(idx > 0 && idx < segment.size());
|
||||||
|
if (idx == 0 || idx >= segment.size()) continue;
|
||||||
|
const size_t cpIdx = segStart + idx;
|
||||||
|
if (cpIdx < cps.size()) {
|
||||||
|
outBreaks.push_back({cps[cpIdx].byteOffset, true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendApostropheContractionBreaks(const std::vector<CodepointInfo>& cps,
|
||||||
|
std::vector<Hyphenator::BreakInfo>& outBreaks) {
|
||||||
|
constexpr size_t kMinLeftSegmentLen = 3;
|
||||||
|
constexpr size_t kMinRightSegmentLen = 3;
|
||||||
|
size_t segmentStart = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < cps.size(); ++i) {
|
||||||
|
if (isSegmentSeparator(cps[i].value)) {
|
||||||
|
if (isApostrophe(cps[i].value) && i > 0 && i + 1 < cps.size() && isAlphabetic(cps[i - 1].value) &&
|
||||||
|
isAlphabetic(cps[i + 1].value)) {
|
||||||
|
size_t leftPrefixLen = 0;
|
||||||
|
for (size_t j = segmentStart; j < i; ++j) {
|
||||||
|
if (isAlphabetic(cps[j].value)) {
|
||||||
|
++leftPrefixLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t rightSuffixLen = 0;
|
||||||
|
for (size_t j = i + 1; j < cps.size() && !isSegmentSeparator(cps[j].value); ++j) {
|
||||||
|
if (isAlphabetic(cps[j].value)) {
|
||||||
|
++rightSuffixLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid stranding short clitics like "l'"/"d'" or contraction tails like "'ve"/"'re"/"'ll".
|
||||||
|
if (leftPrefixLen >= kMinLeftSegmentLen && rightSuffixLen >= kMinRightSegmentLen) {
|
||||||
|
outBreaks.push_back({cps[i + 1].byteOffset, false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segmentStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortAndDedupeBreakInfos(std::vector<Hyphenator::BreakInfo>& infos) {
|
||||||
|
std::sort(infos.begin(), infos.end(), [](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
|
||||||
|
if (a.byteOffset != b.byteOffset) {
|
||||||
|
return a.byteOffset < b.byteOffset;
|
||||||
|
}
|
||||||
|
return a.requiresInsertedHyphen < b.requiresInsertedHyphen;
|
||||||
|
});
|
||||||
|
|
||||||
|
infos.erase(std::unique(infos.begin(), infos.end(),
|
||||||
|
[](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
|
||||||
|
return a.byteOffset == b.byteOffset;
|
||||||
|
}),
|
||||||
|
infos.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
|
||||||
|
if (word.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to codepoints and normalize word boundaries.
|
||||||
|
auto cps = collectCodepoints(word);
|
||||||
|
trimSurroundingPunctuationAndFootnote(cps);
|
||||||
|
const auto* hyphenator = cachedHyphenator_;
|
||||||
|
|
||||||
|
// Detect apostrophe-like separators early; used by both branches below.
|
||||||
|
bool hasApostropheLikeSeparator = false;
|
||||||
|
for (const auto& cp : cps) {
|
||||||
|
if (isApostrophe(cp.value)) {
|
||||||
|
hasApostropheLikeSeparator = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit hyphen markers (soft or hard) take precedence over language breaks.
|
||||||
|
auto explicitBreakInfos = buildExplicitBreakInfos(cps);
|
||||||
|
if (!explicitBreakInfos.empty()) {
|
||||||
|
// When a word contains explicit hyphens we also run Liang patterns on each alphabetic
|
||||||
|
// segment between them. Without this, "US-Satellitensystems" would only offer one split
|
||||||
|
// point (after "US-"), making it impossible to break mid-"Satellitensystems" even when
|
||||||
|
// "US-Satelliten-" would fit on the line.
|
||||||
|
//
|
||||||
|
// Example: "US-Satellitensystems"
|
||||||
|
// Segments: ["US", "Satellitensystems"]
|
||||||
|
// Explicit break: after "US-" -> @3 (no inserted hyphen)
|
||||||
|
// Pattern breaks on "Satellitensystems" -> @5 Sa|tel (+hyphen)
|
||||||
|
// @8 Satel|li (+hyphen)
|
||||||
|
// @10 Satelli|ten (+hyphen)
|
||||||
|
// @13 Satelliten|sys (+hyphen)
|
||||||
|
// @16 Satellitensys|tems (+hyphen)
|
||||||
|
// Result: 6 sorted break points; the line-breaker picks the widest prefix that fits.
|
||||||
|
if (hyphenator) {
|
||||||
|
appendSegmentPatternBreaks(cps, *hyphenator, /*includeFallback=*/false, explicitBreakInfos);
|
||||||
|
}
|
||||||
|
// Also add apostrophe contraction breaks when present (e.g. "l'état-major"
|
||||||
|
// has both an explicit hyphen and an apostrophe that can independently break).
|
||||||
|
if (hasApostropheLikeSeparator) {
|
||||||
|
appendApostropheContractionBreaks(cps, explicitBreakInfos);
|
||||||
|
}
|
||||||
|
// Merge all break points into ascending byte-offset order.
|
||||||
|
sortAndDedupeBreakInfos(explicitBreakInfos);
|
||||||
|
return explicitBreakInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apostrophe-like separators split compounds into alphabetic segments; run Liang on each segment.
|
||||||
|
// This allows words like "all'improvviso" to hyphenate within "improvviso" instead of becoming
|
||||||
|
// completely unsplittable due to the apostrophe punctuation. Apostrophe contraction breaks are
|
||||||
|
// applied regardless of whether a language hyphenator is available.
|
||||||
|
if (hasApostropheLikeSeparator) {
|
||||||
|
std::vector<BreakInfo> segmentedBreaks;
|
||||||
|
if (hyphenator) {
|
||||||
|
appendSegmentPatternBreaks(cps, *hyphenator, includeFallback, segmentedBreaks);
|
||||||
|
}
|
||||||
|
appendApostropheContractionBreaks(cps, segmentedBreaks);
|
||||||
|
sortAndDedupeBreakInfos(segmentedBreaks);
|
||||||
|
return segmentedBreaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask language hyphenator for legal break points.
|
||||||
|
std::vector<size_t> indexes;
|
||||||
|
if (hyphenator) {
|
||||||
|
indexes = hyphenator->breakIndexes(cps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add fallback breaks if needed
|
||||||
|
if (includeFallback && indexes.empty()) {
|
||||||
|
const size_t minPrefix = hyphenator ? hyphenator->minPrefix() : LiangWordConfig::kDefaultMinPrefix;
|
||||||
|
const size_t minSuffix = hyphenator ? hyphenator->minSuffix() : LiangWordConfig::kDefaultMinSuffix;
|
||||||
|
for (size_t idx = minPrefix; idx + minSuffix <= cps.size(); ++idx) {
|
||||||
|
indexes.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexes.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Hyphenator::BreakInfo> breaks;
|
||||||
|
breaks.reserve(indexes.size());
|
||||||
|
for (const size_t idx : indexes) {
|
||||||
|
// CJK characters can break without inserting a visible hyphen.
|
||||||
|
// Check the codepoint at the break position: if it's a CJK character,
|
||||||
|
// no hyphen is needed since CJK scripts don't use hyphenation.
|
||||||
|
bool needsHyphen = true;
|
||||||
|
if (idx < cps.size() && utf8IsCjkBreakable(cps[idx].value)) {
|
||||||
|
needsHyphen = false;
|
||||||
|
} else if (idx > 0 && utf8IsCjkBreakable(cps[idx - 1].value)) {
|
||||||
|
needsHyphen = false;
|
||||||
|
}
|
||||||
|
breaks.push_back({byteOffsetForIndex(cps, idx), needsHyphen});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hyphenator::setPreferredLanguage(const std::string& lang) { cachedHyphenator_ = hyphenatorForLanguage(lang); }
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class LanguageHyphenator;
|
||||||
|
|
||||||
|
class Hyphenator {
|
||||||
|
public:
|
||||||
|
struct BreakInfo {
|
||||||
|
size_t byteOffset; // Byte position inside the UTF-8 word where a break may occur.
|
||||||
|
bool requiresInsertedHyphen; // true = a visible '-' must be rendered at the break (pattern/fallback breaks).
|
||||||
|
// false = break occurs at an existing visible separator boundary
|
||||||
|
// (explicit '-' or eligible apostrophe contraction boundary).
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns byte offsets where the word may be hyphenated.
|
||||||
|
//
|
||||||
|
// Break sources (in priority order):
|
||||||
|
// 1. Explicit hyphens already present in the word (e.g. '-' or soft-hyphen U+00AD).
|
||||||
|
// When found, language patterns are additionally run on each alphabetic segment
|
||||||
|
// between separators so compound words can break within their parts.
|
||||||
|
// Example: "US-Satellitensystems" yields breaks after "US-" (no inserted hyphen)
|
||||||
|
// plus pattern breaks inside "Satellitensystems" (Sa|tel|li|ten|sys|tems).
|
||||||
|
// 2. Apostrophe contractions between letters (e.g. all'improvviso).
|
||||||
|
// Liang patterns are run per alphabetic segment around apostrophes.
|
||||||
|
// A direct break at the apostrophe boundary is allowed only when the left
|
||||||
|
// segment has at least 3 letters and the right segment has at least 3 letters,
|
||||||
|
// avoiding short clitics (e.g. l', d') and contraction tails (e.g. 've, 're, 'll).
|
||||||
|
// 3. Language-specific Liang patterns (e.g. German de_patterns).
|
||||||
|
// Example: "Quadratkilometer" -> Qua|drat|ki|lo|me|ter.
|
||||||
|
// 4. Fallback every-N-chars splitting (only when includeFallback is true AND no
|
||||||
|
// pattern breaks were found). Used as a last resort to prevent a single oversized
|
||||||
|
// word from overflowing the page width.
|
||||||
|
static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback);
|
||||||
|
|
||||||
|
// Provide a publication-level language hint (e.g. "en", "en-US", "ru") used to select hyphenation rules.
|
||||||
|
static void setPreferredLanguage(const std::string& lang);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const LanguageHyphenator* cachedHyphenator_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LiangHyphenation.h"
|
||||||
|
|
||||||
|
// Generic Liang-backed hyphenator that stores pattern metadata plus language-specific helpers.
|
||||||
|
class LanguageHyphenator {
|
||||||
|
public:
|
||||||
|
LanguageHyphenator(const SerializedHyphenationPatterns& patterns, bool (*isLetterFn)(uint32_t),
|
||||||
|
uint32_t (*toLowerFn)(uint32_t), size_t minPrefix = LiangWordConfig::kDefaultMinPrefix,
|
||||||
|
size_t minSuffix = LiangWordConfig::kDefaultMinSuffix)
|
||||||
|
: patterns_(patterns), config_(isLetterFn, toLowerFn, minPrefix, minSuffix) {}
|
||||||
|
|
||||||
|
std::vector<size_t> breakIndexes(const std::vector<CodepointInfo>& cps) const {
|
||||||
|
return liangBreakIndexes(cps, patterns_, config_);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t minPrefix() const { return config_.minPrefix; }
|
||||||
|
size_t minSuffix() const { return config_.minSuffix; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const SerializedHyphenationPatterns& patterns_;
|
||||||
|
LiangWordConfig config_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#include "LanguageRegistry.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "HyphenationCommon.h"
|
||||||
|
#include "generated/hyph-de.trie.h"
|
||||||
|
#include "generated/hyph-en.trie.h"
|
||||||
|
#include "generated/hyph-es.trie.h"
|
||||||
|
#include "generated/hyph-fi.trie.h"
|
||||||
|
#include "generated/hyph-fr.trie.h"
|
||||||
|
#include "generated/hyph-it.trie.h"
|
||||||
|
#include "generated/hyph-pl.trie.h"
|
||||||
|
#include "generated/hyph-ru.trie.h"
|
||||||
|
#include "generated/hyph-sv.trie.h"
|
||||||
|
#include "generated/hyph-uk.trie.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||||
|
LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||||
|
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
|
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
|
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator finnishHyphenator(fi_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
|
using EntryArray = std::array<LanguageEntry, 10>;
|
||||||
|
|
||||||
|
const EntryArray& entries() {
|
||||||
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||||
|
{"french", "fr", &frenchHyphenator},
|
||||||
|
{"german", "de", &germanHyphenator},
|
||||||
|
{"russian", "ru", &russianHyphenator},
|
||||||
|
{"spanish", "es", &spanishHyphenator},
|
||||||
|
{"italian", "it", &italianHyphenator},
|
||||||
|
{"polish", "pl", &polishHyphenator},
|
||||||
|
{"swedish", "sv", &swedishHyphenator},
|
||||||
|
{"ukrainian", "uk", &ukrainianHyphenator},
|
||||||
|
{"finnish", "fi", &finnishHyphenator}}};
|
||||||
|
return kEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) {
|
||||||
|
const auto& allEntries = entries();
|
||||||
|
const auto it = std::find_if(allEntries.begin(), allEntries.end(),
|
||||||
|
[&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; });
|
||||||
|
return (it != allEntries.end()) ? it->hyphenator : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LanguageEntryView getLanguageEntries() {
|
||||||
|
const auto& allEntries = entries();
|
||||||
|
return LanguageEntryView{allEntries.data(), allEntries.size()};
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "LanguageHyphenator.h"
|
||||||
|
|
||||||
|
struct LanguageEntry {
|
||||||
|
const char* cliName;
|
||||||
|
const char* primaryTag;
|
||||||
|
const LanguageHyphenator* hyphenator;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LanguageEntryView {
|
||||||
|
const LanguageEntry* data;
|
||||||
|
size_t size;
|
||||||
|
|
||||||
|
const LanguageEntry* begin() const { return data; }
|
||||||
|
const LanguageEntry* end() const { return data + size; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the Liang-backed hyphenator for a given primary language tag (e.g., "en", "fr").
|
||||||
|
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag);
|
||||||
|
|
||||||
|
// Exposes the list of supported languages primarily for tooling/tests.
|
||||||
|
LanguageEntryView getLanguageEntries();
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
#include "LiangHyphenation.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Liang hyphenation pipeline overview (Typst-style binary trie variant)
|
||||||
|
* --------------------------------------------------------------------
|
||||||
|
* 1. Input normalization (buildAugmentedWord)
|
||||||
|
* - Accepts a vector of CodepointInfo structs emitted by the EPUB text
|
||||||
|
* parser. Each codepoint is validated with LiangWordConfig::isLetter so
|
||||||
|
* we abort early on digits, punctuation, etc. If the word is valid we
|
||||||
|
* build an "augmented" byte sequence: leading '.', lowercase UTF-8 bytes
|
||||||
|
* for every letter, then a trailing '.'. While doing this we capture the
|
||||||
|
* UTF-8 byte offset for each character and a reverse lookup table that
|
||||||
|
* maps UTF-8 byte indexes back to codepoint indexes. This lets the rest
|
||||||
|
* of the algorithm stay byte-oriented (matching the serialized automaton)
|
||||||
|
* while still emitting hyphen positions in codepoint space.
|
||||||
|
*
|
||||||
|
* 2. Automaton decoding
|
||||||
|
* - SerializedHyphenationPatterns stores a contiguous blob generated from
|
||||||
|
* Typst's binary tries. The first 4 bytes contain the root offset. Each
|
||||||
|
* node packs transitions, variable-stride relative offsets to child
|
||||||
|
* nodes, and an optional pointer into a shared "levels" list. We parse
|
||||||
|
* that layout lazily via decodeState/transition, keeping everything in
|
||||||
|
* flash memory; no heap allocations besides the stack-local AutomatonState
|
||||||
|
* structs. getAutomaton caches parseAutomaton results per blob pointer so
|
||||||
|
* multiple words hitting the same language only pay the cost once.
|
||||||
|
*
|
||||||
|
* 3. Pattern application
|
||||||
|
* - We walk the augmented bytes left-to-right. For each starting byte we
|
||||||
|
* stream transitions through the trie, terminating when a transition
|
||||||
|
* fails. Whenever a node exposes level data we expand the packed
|
||||||
|
* "dist+level" bytes: `dist` is the delta (in UTF-8 bytes) from the
|
||||||
|
* starting cursor and `level` is the Liang priority digit. Using the
|
||||||
|
* byte→codepoint lookup we mark the corresponding index in `scores`.
|
||||||
|
* Scores are only updated if the new level is higher, mirroring Liang's
|
||||||
|
* "max digit wins" rule.
|
||||||
|
*
|
||||||
|
* 4. Output filtering
|
||||||
|
* - collectBreakIndexes converts odd-valued score entries back to codepoint
|
||||||
|
* break positions while enforcing `minPrefix`/`minSuffix` constraints from
|
||||||
|
* LiangWordConfig. The caller (language-specific hyphenators) can then
|
||||||
|
* translate these indexes into renderer glyph offsets, page layout data,
|
||||||
|
* etc.
|
||||||
|
*
|
||||||
|
* Keeping the entire algorithm small and deterministic is critical on the
|
||||||
|
* ESP32-C3: we avoid recursion, dynamic allocations per node, or copying the
|
||||||
|
* trie. All lookups stay within the generated blob, which lives in flash, and
|
||||||
|
* the working buffers (augmented bytes/scores) scale with the word length rather
|
||||||
|
* than the pattern corpus.
|
||||||
|
*
|
||||||
|
* Memory design note (heap fragmentation avoidance)
|
||||||
|
* --------------------------------------------------
|
||||||
|
* AugmentedWord previously held three std::vector<> members that were heap-
|
||||||
|
* allocated and freed for every word during layout. For a German-language section
|
||||||
|
* with hundreds of words, these thousands of small alloc/free cycles fragment
|
||||||
|
* the heap enough to prevent large contiguous allocations (e.g. a 32 KB inflate
|
||||||
|
* ring buffer) even when total free memory is sufficient.
|
||||||
|
*
|
||||||
|
* The fix replaces those vectors with fixed-size C arrays sized for the longest
|
||||||
|
* plausible word. The longest known German word is ~63 codepoints; with up to
|
||||||
|
* 2 UTF-8 bytes per German letter + 2 sentinel dots = 128 bytes. MAX_WORD_BYTES=160
|
||||||
|
* and MAX_WORD_CHARS=70 give comfortable headroom. Words exceeding these limits
|
||||||
|
* are silently skipped (no hyphenation), which is acceptable for correctness.
|
||||||
|
* The struct lives on the render-task stack (8 KB) so no permanent DRAM is wasted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using EmbeddedAutomaton = SerializedHyphenationPatterns;
|
||||||
|
|
||||||
|
// Upper bounds for the fixed word buffers. Sized for German (longest known word
|
||||||
|
// ≈63 codepoints × 2 UTF-8 bytes + 2 sentinel dots = 128 bytes). Words that
|
||||||
|
// exceed these limits are skipped rather than heap-allocated.
|
||||||
|
static constexpr size_t MAX_WORD_BYTES = 160; // max UTF-8 bytes in augmented word
|
||||||
|
static constexpr size_t MAX_WORD_CHARS = 70; // max codepoints + 2 sentinel dots
|
||||||
|
|
||||||
|
struct AugmentedWord {
|
||||||
|
uint8_t bytes[MAX_WORD_BYTES];
|
||||||
|
size_t charByteOffsets[MAX_WORD_CHARS];
|
||||||
|
int32_t byteToCharIndex[MAX_WORD_BYTES];
|
||||||
|
size_t byteLen = 0;
|
||||||
|
size_t charCount_ = 0;
|
||||||
|
|
||||||
|
bool empty() const { return byteLen == 0; }
|
||||||
|
size_t charCount() const { return charCount_; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encode a single Unicode codepoint into UTF-8 and append to word.bytes[].
|
||||||
|
// Returns the number of bytes written, or 0 if the codepoint is invalid or the
|
||||||
|
// buffer would overflow. Surrogates (0xD800–0xDFFF) and values above 0x10FFFF
|
||||||
|
// are not valid Unicode scalar values and are rejected.
|
||||||
|
size_t encodeUtf8(uint32_t cp, AugmentedWord& word) {
|
||||||
|
if ((cp >= 0xD800u && cp <= 0xDFFFu) || cp > 0x10FFFFu) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t encoded[4];
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
if (cp <= 0x7Fu) {
|
||||||
|
encoded[len++] = static_cast<uint8_t>(cp);
|
||||||
|
} else if (cp <= 0x7FFu) {
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0xC0u | ((cp >> 6) & 0x1Fu));
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0x80u | (cp & 0x3Fu));
|
||||||
|
} else if (cp <= 0xFFFFu) {
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0xE0u | ((cp >> 12) & 0x0Fu));
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0x80u | ((cp >> 6) & 0x3Fu));
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0x80u | (cp & 0x3Fu));
|
||||||
|
} else {
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0xF0u | ((cp >> 18) & 0x07u));
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0x80u | ((cp >> 12) & 0x3Fu));
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0x80u | ((cp >> 6) & 0x3Fu));
|
||||||
|
encoded[len++] = static_cast<uint8_t>(0x80u | (cp & 0x3Fu));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (word.byteLen + len > MAX_WORD_BYTES) {
|
||||||
|
return 0; // overflow: word too long for fixed buffer, skip hyphenation
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < len; ++i) {
|
||||||
|
word.bytes[word.byteLen++] = encoded[i];
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the dotted, lowercase UTF-8 representation plus lookup tables into `word`.
|
||||||
|
// Returns false if the word should be skipped (empty, non-letter, or too long).
|
||||||
|
bool buildAugmentedWord(AugmentedWord& word, const std::vector<CodepointInfo>& cps, const LiangWordConfig& config) {
|
||||||
|
word.byteLen = 0;
|
||||||
|
word.charCount_ = 0;
|
||||||
|
|
||||||
|
if (cps.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leading sentinel '.'
|
||||||
|
word.charByteOffsets[word.charCount_++] = 0;
|
||||||
|
word.bytes[word.byteLen++] = '.';
|
||||||
|
|
||||||
|
for (const auto& info : cps) {
|
||||||
|
if (!config.isLetter(info.value)) {
|
||||||
|
word.byteLen = 0;
|
||||||
|
word.charCount_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Reserve one slot for the trailing sentinel and check byte headroom.
|
||||||
|
if (word.charCount_ >= MAX_WORD_CHARS - 1) {
|
||||||
|
word.byteLen = 0;
|
||||||
|
word.charCount_ = 0;
|
||||||
|
return false; // word too long
|
||||||
|
}
|
||||||
|
word.charByteOffsets[word.charCount_++] = word.byteLen;
|
||||||
|
if (encodeUtf8(config.toLower(info.value), word) == 0) {
|
||||||
|
word.byteLen = 0;
|
||||||
|
word.charCount_ = 0;
|
||||||
|
return false; // byte buffer overflow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing sentinel '.'
|
||||||
|
if (word.charCount_ >= MAX_WORD_CHARS || word.byteLen >= MAX_WORD_BYTES) {
|
||||||
|
word.byteLen = 0;
|
||||||
|
word.charCount_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
word.charByteOffsets[word.charCount_++] = word.byteLen;
|
||||||
|
word.bytes[word.byteLen++] = '.';
|
||||||
|
|
||||||
|
// Build byte→char reverse index: -1 for mid-codepoint bytes, char index for start bytes.
|
||||||
|
for (size_t i = 0; i < word.byteLen; ++i) {
|
||||||
|
word.byteToCharIndex[i] = -1;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < word.charCount_; ++i) {
|
||||||
|
const size_t offset = word.charByteOffsets[i];
|
||||||
|
if (offset < word.byteLen) {
|
||||||
|
word.byteToCharIndex[offset] = static_cast<int32_t>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoded view of a single trie node pulled straight out of the serialized blob.
|
||||||
|
// - transitions: contiguous list of next-byte values
|
||||||
|
// - targets: packed relative offsets (1/2/3 bytes) for each transition
|
||||||
|
// - levels: optional pointer into the global levels list with packed dist/level pairs
|
||||||
|
struct AutomatonState {
|
||||||
|
const uint8_t* data = nullptr;
|
||||||
|
size_t size = 0;
|
||||||
|
size_t addr = 0;
|
||||||
|
uint8_t stride = 1;
|
||||||
|
size_t childCount = 0;
|
||||||
|
const uint8_t* transitions = nullptr;
|
||||||
|
const uint8_t* targets = nullptr;
|
||||||
|
const uint8_t* levels = nullptr;
|
||||||
|
size_t levelsLen = 0;
|
||||||
|
|
||||||
|
bool valid() const { return data != nullptr; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interpret the node located at `addr`, returning transition metadata.
|
||||||
|
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
||||||
|
AutomatonState state;
|
||||||
|
if (addr >= automaton.size) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* base = automaton.data + addr;
|
||||||
|
size_t remaining = automaton.size - addr;
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
const uint8_t header = base[pos++];
|
||||||
|
// Header layout (bits):
|
||||||
|
// 7 - hasLevels flag
|
||||||
|
// 6..5 - stride selector (0 -> 1 byte, otherwise 1|2|3)
|
||||||
|
// 4..0 - child count (5 bits), 31 == overflow -> extra byte
|
||||||
|
const bool hasLevels = (header >> 7) != 0;
|
||||||
|
uint8_t stride = static_cast<uint8_t>((header >> 5) & 0x03u);
|
||||||
|
if (stride == 0) {
|
||||||
|
stride = 1;
|
||||||
|
}
|
||||||
|
size_t childCount = static_cast<size_t>(header & 0x1Fu);
|
||||||
|
if (childCount == 31u) {
|
||||||
|
if (pos >= remaining) {
|
||||||
|
return AutomatonState{};
|
||||||
|
}
|
||||||
|
childCount = base[pos++];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* levelsPtr = nullptr;
|
||||||
|
size_t levelsLen = 0;
|
||||||
|
if (hasLevels) {
|
||||||
|
if (pos + 1 >= remaining) {
|
||||||
|
return AutomatonState{};
|
||||||
|
}
|
||||||
|
const uint8_t offsetHi = base[pos++];
|
||||||
|
const uint8_t offsetLoLen = base[pos++];
|
||||||
|
// The 12-bit offset (hi<<4 | top nibble) points into the blob-level levels list.
|
||||||
|
// The bottom nibble stores how many packed entries belong to this node.
|
||||||
|
const size_t offset = (static_cast<size_t>(offsetHi) << 4) | (offsetLoLen >> 4);
|
||||||
|
levelsLen = offsetLoLen & 0x0Fu;
|
||||||
|
if (offset + levelsLen > automaton.size) {
|
||||||
|
return AutomatonState{};
|
||||||
|
}
|
||||||
|
levelsPtr = automaton.data + offset - 4u;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos + childCount > remaining) {
|
||||||
|
return AutomatonState{};
|
||||||
|
}
|
||||||
|
const uint8_t* transitions = base + pos;
|
||||||
|
pos += childCount;
|
||||||
|
|
||||||
|
const size_t targetsBytes = childCount * stride;
|
||||||
|
if (pos + targetsBytes > remaining) {
|
||||||
|
return AutomatonState{};
|
||||||
|
}
|
||||||
|
const uint8_t* targets = base + pos;
|
||||||
|
|
||||||
|
state.data = automaton.data;
|
||||||
|
state.size = automaton.size;
|
||||||
|
state.addr = addr;
|
||||||
|
state.stride = stride;
|
||||||
|
state.childCount = childCount;
|
||||||
|
state.transitions = transitions;
|
||||||
|
state.targets = targets;
|
||||||
|
state.levels = levelsPtr;
|
||||||
|
state.levelsLen = levelsLen;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the packed stride-sized delta back into a signed offset.
|
||||||
|
int32_t decodeDelta(const uint8_t* buf, uint8_t stride) {
|
||||||
|
if (stride == 1) {
|
||||||
|
return static_cast<int8_t>(buf[0]);
|
||||||
|
}
|
||||||
|
if (stride == 2) {
|
||||||
|
return static_cast<int16_t>((static_cast<uint16_t>(buf[0]) << 8) | static_cast<uint16_t>(buf[1]));
|
||||||
|
}
|
||||||
|
const int32_t unsignedVal =
|
||||||
|
(static_cast<int32_t>(buf[0]) << 16) | (static_cast<int32_t>(buf[1]) << 8) | static_cast<int32_t>(buf[2]);
|
||||||
|
return unsignedVal - (1 << 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow a single byte transition from `state`, decoding the child node on success.
|
||||||
|
bool transition(const EmbeddedAutomaton& automaton, const AutomatonState& state, uint8_t letter, AutomatonState& out) {
|
||||||
|
if (!state.valid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children remain sorted by letter in the serialized blob, but the lists are
|
||||||
|
// short enough that a linear scan keeps code size down compared to binary search.
|
||||||
|
for (size_t idx = 0; idx < state.childCount; ++idx) {
|
||||||
|
if (state.transitions[idx] != letter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint8_t* deltaPtr = state.targets + idx * state.stride;
|
||||||
|
const int32_t delta = decodeDelta(deltaPtr, state.stride);
|
||||||
|
// Deltas are relative to the current node's address, allowing us to keep all
|
||||||
|
// targets within 24 bits while still referencing further nodes in the blob.
|
||||||
|
const int64_t nextAddr = static_cast<int64_t>(state.addr) + delta;
|
||||||
|
if (nextAddr < 0 || static_cast<size_t>(nextAddr) >= automaton.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = decodeState(automaton, static_cast<size_t>(nextAddr));
|
||||||
|
return out.valid();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts odd score positions back into codepoint indexes, honoring min prefix/suffix constraints.
|
||||||
|
// Each break corresponds to scores[breakIndex + 1] because of the leading '.' sentinel.
|
||||||
|
// Convert odd score entries into hyphen positions while honoring prefix/suffix limits.
|
||||||
|
std::vector<size_t> collectBreakIndexes(const std::vector<CodepointInfo>& cps, const uint8_t* scores,
|
||||||
|
const size_t scoresSize, const size_t minPrefix, const size_t minSuffix) {
|
||||||
|
std::vector<size_t> indexes;
|
||||||
|
const size_t cpCount = cps.size();
|
||||||
|
if (cpCount < 2) {
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t breakIndex = 1; breakIndex < cpCount; ++breakIndex) {
|
||||||
|
if (breakIndex < minPrefix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t suffixCount = cpCount - breakIndex;
|
||||||
|
if (suffixCount < minSuffix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t scoreIdx = breakIndex + 1;
|
||||||
|
if (scoreIdx >= scoresSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((scores[scoreIdx] & 1u) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
indexes.push_back(breakIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Entry point that runs the full Liang pipeline for a single word.
|
||||||
|
std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
|
||||||
|
const SerializedHyphenationPatterns& patterns, const LiangWordConfig& config) {
|
||||||
|
// AugmentedWord uses fixed-size C arrays (no heap allocation) to avoid
|
||||||
|
// fragmenting the heap across hundreds of words during page layout.
|
||||||
|
AugmentedWord augmented;
|
||||||
|
if (!buildAugmentedWord(augmented, cps, config)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbeddedAutomaton& automaton = patterns;
|
||||||
|
|
||||||
|
const AutomatonState root = decodeState(automaton, automaton.rootOffset);
|
||||||
|
if (!root.valid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liang scores: one entry per augmented char (leading/trailing dots included).
|
||||||
|
// Stack-allocated to avoid heap fragmentation (see memory design note above).
|
||||||
|
uint8_t scores[MAX_WORD_CHARS];
|
||||||
|
for (size_t i = 0; i < augmented.charCount_; ++i) {
|
||||||
|
scores[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk every starting character position and stream bytes through the trie.
|
||||||
|
for (size_t charStart = 0; charStart < augmented.charCount_; ++charStart) {
|
||||||
|
const size_t byteStart = augmented.charByteOffsets[charStart];
|
||||||
|
AutomatonState state = root;
|
||||||
|
|
||||||
|
for (size_t cursor = byteStart; cursor < augmented.byteLen; ++cursor) {
|
||||||
|
AutomatonState next;
|
||||||
|
if (!transition(automaton, state, augmented.bytes[cursor], next)) {
|
||||||
|
break; // No more matches for this prefix.
|
||||||
|
}
|
||||||
|
state = next;
|
||||||
|
|
||||||
|
if (state.levels && state.levelsLen > 0) {
|
||||||
|
size_t offset = 0;
|
||||||
|
// Each packed byte stores the byte-distance delta and the Liang level digit.
|
||||||
|
for (size_t i = 0; i < state.levelsLen; ++i) {
|
||||||
|
const uint8_t packed = state.levels[i];
|
||||||
|
const size_t dist = static_cast<size_t>(packed / 10);
|
||||||
|
const uint8_t level = static_cast<uint8_t>(packed % 10);
|
||||||
|
|
||||||
|
offset += dist;
|
||||||
|
const size_t splitByte = byteStart + offset;
|
||||||
|
if (splitByte >= augmented.byteLen) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t boundary = augmented.byteToCharIndex[splitByte];
|
||||||
|
if (boundary < 0) {
|
||||||
|
continue; // Mid-codepoint byte, wait for the next one.
|
||||||
|
}
|
||||||
|
if (boundary < 2 || boundary + 2 > static_cast<int32_t>(augmented.charCount_)) {
|
||||||
|
continue; // Skip splits that land in the leading/trailing sentinels.
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t idx = static_cast<size_t>(boundary);
|
||||||
|
if (idx >= augmented.charCount_) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scores[idx] = std::max(scores[idx], level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectBreakIndexes(cps, scores, augmented.charCount_, config.minPrefix, config.minSuffix);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "HyphenationCommon.h"
|
||||||
|
#include "SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Encapsulates every language-specific dial the Liang algorithm needs at runtime. The helpers are
|
||||||
|
// intentionally represented as bare function pointers because we invoke them inside tight loops and
|
||||||
|
// want to avoid the overhead of std::function or functors. The minima default to the TeX-recommended
|
||||||
|
// "2/2" split but individual languages (English, for example) can override them.
|
||||||
|
struct LiangWordConfig {
|
||||||
|
static constexpr size_t kDefaultMinPrefix = 2;
|
||||||
|
static constexpr size_t kDefaultMinSuffix = 2;
|
||||||
|
// Predicate used to reject non-alphabetic characters before pattern lookup. Returning false causes
|
||||||
|
// the entire word to be skipped, matching the behavior of classic TeX hyphenation tables.
|
||||||
|
bool (*isLetter)(uint32_t);
|
||||||
|
// Language-specific case folding that matches how the TeX patterns were authored (usually lower-case
|
||||||
|
// ASCII for Latin and lowercase Cyrillic for Russian). Patterns are stored in UTF-8, so this must
|
||||||
|
// operate on Unicode scalars rather than bytes.
|
||||||
|
uint32_t (*toLower)(uint32_t);
|
||||||
|
// Minimum codepoints required on the left/right of any break. These correspond to TeX's
|
||||||
|
// lefthyphenmin and righthyphenmin knobs.
|
||||||
|
size_t minPrefix;
|
||||||
|
size_t minSuffix;
|
||||||
|
|
||||||
|
// Lightweight aggregate constructor so call sites can declare `const LiangWordConfig config(...)`
|
||||||
|
// without verbose member assignment boilerplate.
|
||||||
|
LiangWordConfig(bool (*letterFn)(uint32_t), uint32_t (*lowerFn)(uint32_t), size_t prefix = kDefaultMinPrefix,
|
||||||
|
size_t suffix = kDefaultMinSuffix)
|
||||||
|
: isLetter(letterFn), toLower(lowerFn), minPrefix(prefix), minSuffix(suffix) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shared Liang pattern evaluator used by every language-specific hyphenator.
|
||||||
|
std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
|
||||||
|
const SerializedHyphenationPatterns& patterns, const LiangWordConfig& config);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
|
||||||
|
struct SerializedHyphenationPatterns {
|
||||||
|
size_t rootOffset;
|
||||||
|
const std::uint8_t* data;
|
||||||
|
size_t size;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,869 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t es_trie_data[] = {
|
||||||
|
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 = {
|
||||||
|
0x34F8u,
|
||||||
|
es_trie_data,
|
||||||
|
sizeof(es_trie_data),
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t fi_trie_data[] = {
|
||||||
|
0x01, 0x01, 0x16, 0x0B, 0x0C, 0x17, 0x0C, 0x15, 0x0C, 0x0B, 0x16, 0x29, 0x2B, 0x20, 0x1F, 0x0C,
|
||||||
|
0x33, 0x02, 0x0B, 0x02, 0x0B, 0x0C, 0x01, 0x0C, 0x34, 0x0B, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x20,
|
||||||
|
0x0B, 0x21, 0xA0, 0x00, 0x41, 0xA0, 0x02, 0x51, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00,
|
||||||
|
0x41, 0x62, 0xFD, 0xA0, 0x01, 0xA2, 0xA1, 0x00, 0x81, 0x6F, 0xFD, 0xA3, 0x00, 0x81, 0x69, 0x6F,
|
||||||
|
0x75, 0xF8, 0xF8, 0xF8, 0x28, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72, 0xDE, 0xDE, 0xEA,
|
||||||
|
0xDE, 0xDE, 0xDE, 0xF2, 0xF7, 0x22, 0xA4, 0xB6, 0xCD, 0xCD, 0xA1, 0x00, 0x81, 0x61, 0xD9, 0x28,
|
||||||
|
0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x72, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xF6, 0xFB,
|
||||||
|
0xA2, 0x00, 0x81, 0x61, 0x65, 0xC3, 0xC3, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72,
|
||||||
|
0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xE3, 0xFF, 0xF9,
|
||||||
|
0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92,
|
||||||
|
0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0xC5, 0xFF, 0xA6, 0xFF, 0xCA, 0x47, 0x61, 0x65, 0x69,
|
||||||
|
0x6F, 0x75, 0x79, 0xC3, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76,
|
||||||
|
0xFF, 0xA9, 0xA0, 0x00, 0xF1, 0x21, 0x73, 0xFD, 0xA1, 0x00, 0x41, 0x75, 0xFD, 0xA0, 0x00, 0x81,
|
||||||
|
0x43, 0x61, 0x65, 0x69, 0xFF, 0x63, 0xFF, 0x63, 0xFF, 0x63, 0xC1, 0x01, 0xA2, 0x61, 0xFF, 0x59,
|
||||||
|
0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0x76, 0xFF, 0x42, 0xFF, 0xE8, 0xFF,
|
||||||
|
0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x75, 0xFF, 0xED, 0xFF, 0xF0, 0xFF, 0xFA, 0xA1,
|
||||||
|
0x00, 0x41, 0x73, 0xCE, 0x47, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xFB, 0xFF, 0x1E,
|
||||||
|
0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x51, 0xA0, 0x01, 0x52, 0x21, 0x6F, 0xFD,
|
||||||
|
0x22, 0x74, 0x6E, 0xFD, 0xFD, 0xA0, 0x01, 0x73, 0x21, 0x6E, 0xFD, 0x22, 0x61, 0x6F, 0xFD, 0xFA,
|
||||||
|
0x21, 0x61, 0xEA, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xF2, 0xA4, 0x00, 0x41, 0x6E, 0x6A, 0x69, 0x6C,
|
||||||
|
0xE7, 0xF2, 0xFA, 0xFD, 0x21, 0x73, 0xE1, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41, 0x64, 0xFD, 0x21,
|
||||||
|
0x74, 0xD6, 0x21, 0x73, 0xFD, 0x22, 0x65, 0x69, 0xFA, 0xFD, 0x21, 0x61, 0xCB, 0x21, 0x6E, 0xBD,
|
||||||
|
0x22, 0x74, 0x6F, 0xBD, 0xFD, 0x21, 0x69, 0xC0, 0x21, 0x61, 0xFD, 0xA4, 0x00, 0x41, 0x70, 0x73,
|
||||||
|
0x74, 0x6D, 0xEA, 0xEF, 0xF5, 0xFD, 0x21, 0x69, 0xD9, 0xA1, 0x00, 0x41, 0x6C, 0xFD, 0x47, 0x61,
|
||||||
|
0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xBB, 0xFF, 0xCC, 0xFE, 0xA4, 0xFF, 0xED, 0xFE, 0xA4,
|
||||||
|
0xFF, 0xFB, 0xFE, 0xD7, 0xA0, 0x01, 0x41, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41,
|
||||||
|
0x72, 0xFD, 0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFE, 0x80, 0xFF, 0xFB,
|
||||||
|
0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0xB3, 0xFF, 0x2B, 0xFE, 0x94, 0x21, 0x61,
|
||||||
|
0xDC, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0xFF, 0x3E, 0xFE, 0x61, 0xFE, 0x61,
|
||||||
|
0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x94, 0xFF, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0x80, 0xFF,
|
||||||
|
0x40, 0x42, 0x6F, 0x65, 0xFF, 0x84, 0xFF, 0x47, 0x41, 0x75, 0xFF, 0x32, 0x21, 0x74, 0xFC, 0x42,
|
||||||
|
0x61, 0x6F, 0xFF, 0xFD, 0xFF, 0x36, 0xA4, 0x00, 0x41, 0x73, 0x6C, 0x6A, 0x70, 0xE4, 0xEB, 0xF9,
|
||||||
|
0xF2, 0x41, 0x79, 0xFF, 0x24, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x41, 0x73, 0xFD,
|
||||||
|
0x42, 0x2E, 0x6E, 0xFF, 0x15, 0xFF, 0x15, 0x21, 0x61, 0xF9, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x41,
|
||||||
|
0x64, 0xFD, 0x41, 0x65, 0xFE, 0xF8, 0x21, 0x6A, 0xFC, 0x42, 0x6B, 0x74, 0xFE, 0xFC, 0xFE, 0xFC,
|
||||||
|
0x21, 0x73, 0xF9, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0x41, 0x68, 0x70, 0x73, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||||
|
0xFF, 0x24, 0x41, 0x74, 0xFF, 0x23, 0xC2, 0x00, 0x41, 0x72, 0x68, 0xFF, 0x30, 0xFF, 0xFC, 0xA0,
|
||||||
|
0x00, 0x52, 0x21, 0x72, 0xFD, 0x21, 0x69, 0xFA, 0x21, 0x6C, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x68,
|
||||||
|
0xFD, 0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0x70, 0x63, 0xFF, 0x95, 0xFF, 0xAA,
|
||||||
|
0xFF, 0xBC, 0xFF, 0xD5, 0xFD, 0xC1, 0xFF, 0xE5, 0xFD, 0xF4, 0xFF, 0xF1, 0xFF, 0xF7, 0xFF, 0xFD,
|
||||||
|
0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x73, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD,
|
||||||
|
0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xD5, 0xFF, 0xDE, 0xA0, 0x00, 0x92, 0xA0, 0x00, 0xB2, 0xA0,
|
||||||
|
0x01, 0x01, 0xC3, 0x00, 0x61, 0x69, 0x65, 0x79, 0xFE, 0x20, 0xFE, 0x20, 0xFF, 0xFD, 0x22, 0xA4,
|
||||||
|
0xB6, 0xF4, 0xAD, 0x25, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0xA8, 0xE6, 0xE6, 0xE9, 0xFB, 0x42, 0xB6,
|
||||||
|
0xA4, 0xFF, 0x9D, 0xFF, 0x9D, 0x46, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0x65, 0xFF, 0x96, 0xFF, 0xD4,
|
||||||
|
0xFF, 0xD4, 0xFF, 0xD7, 0xFF, 0xF9, 0xFF, 0xD7, 0x22, 0xA4, 0xB6, 0xDB, 0xED, 0xA0, 0x00, 0x72,
|
||||||
|
0xA0, 0x00, 0x71, 0x21, 0xA4, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xA4, 0xFD, 0x21, 0x69, 0xF4, 0xA0,
|
||||||
|
0x01, 0x22, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x26, 0x61, 0x6F, 0x75, 0xC3,
|
||||||
|
0x65, 0x6C, 0xE2, 0xE2, 0xE2, 0xEE, 0xF1, 0xFD, 0x22, 0xA4, 0xB6, 0xD8, 0xD8, 0x21, 0x61, 0xD3,
|
||||||
|
0xA0, 0x00, 0xB1, 0x24, 0x75, 0x69, 0x65, 0x6F, 0xCD, 0xCD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F,
|
||||||
|
0x75, 0xF4, 0xF4, 0xF4, 0xF4, 0x25, 0x79, 0xC3, 0x61, 0x75, 0x69, 0xBB, 0xE3, 0xE8, 0xEE, 0xF7,
|
||||||
|
0xA0, 0x00, 0xD2, 0x22, 0xA4, 0xB6, 0xFD, 0xFD, 0x44, 0x61, 0x65, 0x6F, 0x69, 0xFF, 0x64, 0xFF,
|
||||||
|
0x64, 0xFF, 0x64, 0xFF, 0x64, 0x42, 0x65, 0x61, 0xFF, 0x9B, 0xFF, 0xCB, 0x21, 0x65, 0xC4, 0x22,
|
||||||
|
0x61, 0x75, 0xC1, 0xC1, 0xA0, 0x02, 0x32, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x49, 0x79, 0xC3,
|
||||||
|
0x75, 0x61, 0x65, 0x69, 0x6F, 0x73, 0x6C, 0xFF, 0x80, 0xFF, 0xD6, 0xFF, 0xDB, 0xFF, 0xB0, 0xFF,
|
||||||
|
0xE8, 0xFF, 0xEF, 0xFF, 0xF2, 0xFD, 0x70, 0xFF, 0xFD, 0x44, 0x69, 0x65, 0x6F, 0x75, 0xFF, 0x23,
|
||||||
|
0xFF, 0x23, 0xFF, 0x23, 0xFF, 0x23, 0x43, 0x75, 0x61, 0x65, 0xFF, 0x5A, 0xFF, 0x8A, 0xFF, 0x8A,
|
||||||
|
0x41, 0x76, 0xFF, 0x5F, 0x21, 0x61, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD,
|
||||||
|
0x21, 0x65, 0xFD, 0x43, 0x69, 0x6F, 0x6B, 0xFF, 0xF1, 0xFD, 0xF7, 0xFF, 0xFD, 0xA0, 0x01, 0xA4,
|
||||||
|
0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x43, 0x6E, 0x74, 0x6B, 0xFC, 0x7D, 0xFC, 0x7D, 0xFF, 0xFD,
|
||||||
|
0x41, 0x69, 0xFC, 0x73, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x48, 0xC3, 0x61, 0x75,
|
||||||
|
0x65, 0x6F, 0x69, 0x6C, 0x73, 0xFF, 0x3C, 0xFF, 0xAD, 0xFF, 0xBA, 0xFF, 0x20, 0xFF, 0x20, 0xFF,
|
||||||
|
0x50, 0xFF, 0xD7, 0xFF, 0xFD, 0x44, 0x61, 0x69, 0x75, 0x79, 0xFE, 0xB7, 0xFE, 0xB7, 0xFE, 0xB7,
|
||||||
|
0xFE, 0xB7, 0x42, 0x61, 0x69, 0xFE, 0xEE, 0xFE, 0xEE, 0x42, 0x75, 0x61, 0xFE, 0xE7, 0xFF, 0x17,
|
||||||
|
0x42, 0xA4, 0xB6, 0xFE, 0xE6, 0xFF, 0x30, 0x24, 0x65, 0x61, 0x75, 0xC3, 0xDE, 0xEB, 0xF2, 0xF9,
|
||||||
|
0x43, 0x61, 0x65, 0x6F, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0x42, 0x61, 0x75, 0xFE, 0xC6, 0xFE,
|
||||||
|
0xC6, 0x44, 0x75, 0x61, 0x65, 0x6F, 0xFE, 0xBF, 0xFE, 0xEF, 0xFE, 0xEF, 0xFE, 0xEF, 0x41, 0xB6,
|
||||||
|
0xFE, 0xB2, 0x21, 0xC3, 0xFC, 0x42, 0xA4, 0xB6, 0xFE, 0xB1, 0xFF, 0xFD, 0x43, 0x61, 0x6F, 0x79,
|
||||||
|
0xFE, 0xD4, 0xFE, 0xD4, 0xFE, 0xD4, 0x42, 0x61, 0x65, 0xFE, 0x56, 0xFE, 0x56, 0x26, 0x69, 0x61,
|
||||||
|
0x75, 0xC3, 0x65, 0x6F, 0xC3, 0xCD, 0xD4, 0xE8, 0xEF, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0xA4, 0xFD,
|
||||||
|
0xA0, 0x01, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x02, 0x03, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x75, 0xFD, 0x23, 0xC3, 0x79, 0x73, 0xE2,
|
||||||
|
0xEE, 0xFD, 0x41, 0x72, 0xFD, 0xD9, 0x42, 0x6C, 0x68, 0xFC, 0x47, 0xFF, 0xFC, 0xC1, 0x00, 0x81,
|
||||||
|
0x69, 0xFB, 0xA6, 0x21, 0x76, 0xFA, 0x59, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0xC3, 0x79, 0x6F, 0x75, 0x61, 0x65, 0x69, 0x2E, 0x63, 0x71,
|
||||||
|
0xFB, 0xAE, 0xFB, 0xC9, 0xFB, 0xE1, 0xFB, 0xFA, 0xFC, 0x16, 0xFC, 0x16, 0xFC, 0x4A, 0xFC, 0x6E,
|
||||||
|
0xFC, 0x16, 0xFC, 0xE8, 0xFD, 0x0C, 0xFD, 0x2B, 0xFD, 0xCB, 0xFD, 0xEA, 0xFC, 0x16, 0xFE, 0x42,
|
||||||
|
0xFE, 0x65, 0xFE, 0x8F, 0xFE, 0xC7, 0xFF, 0x36, 0xFF, 0x71, 0xFF, 0xB7, 0xFF, 0xE5, 0xFF, 0xF0,
|
||||||
|
0xFF, 0xFD,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns fi_patterns = {
|
||||||
|
0x496u,
|
||||||
|
fi_trie_data,
|
||||||
|
sizeof(fi_trie_data),
|
||||||
|
};
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t fr_trie_data[] = {
|
||||||
|
0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x2B,
|
||||||
|
0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17,
|
||||||
|
0x04, 0x1F, 0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36,
|
||||||
|
0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C,
|
||||||
|
0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B,
|
||||||
|
0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x48, 0x2C, 0x0B, 0x29, 0x16,
|
||||||
|
0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x3E, 0x0D,
|
||||||
|
0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B,
|
||||||
|
0x16, 0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD,
|
||||||
|
0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||||
|
0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63,
|
||||||
|
0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0,
|
||||||
|
0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
||||||
|
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x6E, 0xFD, 0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61,
|
||||||
|
0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA,
|
||||||
|
0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD,
|
||||||
|
0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x5E, 0x21, 0x74, 0xFC,
|
||||||
|
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x74,
|
||||||
|
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70,
|
||||||
|
0x73, 0x72, 0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF,
|
||||||
|
0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB,
|
||||||
|
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0,
|
||||||
|
0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0x6E, 0x75, 0xF2, 0xFD, 0x21,
|
||||||
|
0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0xFF, 0x06,
|
||||||
|
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21,
|
||||||
|
0x74, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2,
|
||||||
|
0x02, 0x52, 0x6E, 0x75, 0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD,
|
||||||
|
0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70,
|
||||||
|
0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD,
|
||||||
|
0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x72, 0xFD, 0x21,
|
||||||
|
0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
||||||
|
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96,
|
||||||
|
0xFF, 0x96, 0x41, 0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52,
|
||||||
|
0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05,
|
||||||
|
0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75,
|
||||||
|
0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0xC2, 0x02,
|
||||||
|
0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74,
|
||||||
|
0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF,
|
||||||
|
0xF4, 0xFF, 0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7,
|
||||||
|
0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD,
|
||||||
|
0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||||
|
0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xFD, 0xA0, 0x00, 0x61,
|
||||||
|
0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0xFE, 0xC8,
|
||||||
|
0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43,
|
||||||
|
0x64, 0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E,
|
||||||
|
0x70, 0x73, 0x72, 0x67, 0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D,
|
||||||
|
0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||||
|
0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2,
|
||||||
|
0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x47, 0xFE, 0x47,
|
||||||
|
0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
||||||
|
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43,
|
||||||
|
0x63, 0x74, 0x75, 0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F,
|
||||||
|
0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0,
|
||||||
|
0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75,
|
||||||
|
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||||
|
0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0x6C,
|
||||||
|
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD,
|
||||||
|
0xA0, 0x04, 0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21,
|
||||||
|
0xC3, 0xFD, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD,
|
||||||
|
0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02,
|
||||||
|
0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
|
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x31, 0x21,
|
||||||
|
0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41,
|
||||||
|
0x61, 0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41,
|
||||||
|
0x6F, 0xFE, 0x7B, 0xA0, 0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21,
|
||||||
|
0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63,
|
||||||
|
0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3,
|
||||||
|
0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x74,
|
||||||
|
0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
||||||
|
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E,
|
||||||
|
0x72, 0x73, 0xFF, 0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9,
|
||||||
|
0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C,
|
||||||
|
0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1,
|
||||||
|
0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x23, 0x21, 0x6E, 0xFD,
|
||||||
|
0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0xFF,
|
||||||
|
0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF,
|
||||||
|
0xFD, 0x44, 0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9,
|
||||||
|
0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64,
|
||||||
|
0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C,
|
||||||
|
0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x68, 0xFC, 0x92, 0x23, 0x61,
|
||||||
|
0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0xFC, 0x5A,
|
||||||
|
0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79,
|
||||||
|
0x6F, 0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69,
|
||||||
|
0x6F, 0x73, 0x74, 0x75, 0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0,
|
||||||
|
0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE,
|
||||||
|
0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63,
|
||||||
|
0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x6E, 0xFB, 0x41,
|
||||||
|
0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
||||||
|
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21,
|
||||||
|
0xC3, 0xFC, 0x21, 0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3,
|
||||||
|
0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64,
|
||||||
|
0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD,
|
||||||
|
0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xFE, 0xCA, 0x21, 0x6F,
|
||||||
|
0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0x44,
|
||||||
|
0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5,
|
||||||
|
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD,
|
||||||
|
0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21,
|
||||||
|
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA,
|
||||||
|
0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xFA, 0xA4, 0xFA, 0xA4, 0xFF,
|
||||||
|
0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0xFD, 0x44,
|
||||||
|
0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5,
|
||||||
|
0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41,
|
||||||
|
0xA9, 0xFC, 0x27, 0x21, 0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21,
|
||||||
|
0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2,
|
||||||
|
0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73,
|
||||||
|
0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0x21, 0xA9,
|
||||||
|
0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
||||||
|
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4,
|
||||||
|
0xFC, 0xBD, 0x21, 0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93,
|
||||||
|
0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9,
|
||||||
|
0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD,
|
||||||
|
0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xA0, 0x01, 0xC1, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x81,
|
||||||
|
0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41,
|
||||||
|
0x73, 0xFE, 0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD,
|
||||||
|
0x43, 0x6F, 0x73, 0x75, 0xFF, 0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD,
|
||||||
|
0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0x61, 0xFE, 0xA9, 0x21, 0x69,
|
||||||
|
0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0x21, 0x74,
|
||||||
|
0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD,
|
||||||
|
0x25, 0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB,
|
||||||
|
0xFD, 0x21, 0x61, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD,
|
||||||
|
0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68,
|
||||||
|
0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA,
|
||||||
|
0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xFB,
|
||||||
|
0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
||||||
|
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7,
|
||||||
|
0xFF, 0xFD, 0x41, 0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C,
|
||||||
|
0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80,
|
||||||
|
0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9,
|
||||||
|
0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0xD7, 0xFF, 0xE4, 0xFD,
|
||||||
|
0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xB9,
|
||||||
|
0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21,
|
||||||
|
0xA9, 0xFD, 0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9,
|
||||||
|
0x42, 0x66, 0x78, 0xFB, 0x18, 0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1,
|
||||||
|
0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41,
|
||||||
|
0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x63, 0x68, 0x75, 0x6F, 0xFF,
|
||||||
|
0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0xFD, 0xC3,
|
||||||
|
0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43,
|
||||||
|
0x63, 0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F,
|
||||||
|
0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41,
|
||||||
|
0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||||
|
0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
|
0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0xF8, 0xFF, 0xF9,
|
||||||
|
0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
||||||
|
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41,
|
||||||
|
0x69, 0xF7, 0xD2, 0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73,
|
||||||
|
0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF,
|
||||||
|
0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70,
|
||||||
|
0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x41, 0x75, 0xF8, 0x30,
|
||||||
|
0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0xF8,
|
||||||
|
0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41,
|
||||||
|
0x73, 0xF8, 0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41,
|
||||||
|
0x69, 0xF8, 0x73, 0x21, 0x75, 0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA,
|
||||||
|
0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72,
|
||||||
|
0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0xBF, 0xF6, 0xBF, 0x42, 0x63,
|
||||||
|
0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x74,
|
||||||
|
0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2,
|
||||||
|
0x61, 0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF,
|
||||||
|
0xF9, 0xF6, 0x99, 0xF6, 0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF,
|
||||||
|
0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF,
|
||||||
|
0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xC3, 0x62, 0x63, 0x64, 0x65, 0x69,
|
||||||
|
0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0xB1, 0xF8, 0xE6,
|
||||||
|
0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
||||||
|
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85,
|
||||||
|
0xF8, 0x85, 0xA0, 0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21,
|
||||||
|
0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73,
|
||||||
|
0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21,
|
||||||
|
0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F,
|
||||||
|
0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x61,
|
||||||
|
0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
||||||
|
0xFD, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74,
|
||||||
|
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74,
|
||||||
|
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0xE1, 0xFD, 0x41, 0x74, 0xFE,
|
||||||
|
0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xF6, 0xFD,
|
||||||
|
0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41,
|
||||||
|
0x2E, 0xFF, 0x33, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63,
|
||||||
|
0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22,
|
||||||
|
0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||||
|
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
||||||
|
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E,
|
||||||
|
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD,
|
||||||
|
0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64,
|
||||||
|
0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6,
|
||||||
|
0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xC9, 0xFF, 0xD4,
|
||||||
|
0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||||
|
0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02,
|
||||||
|
0x41, 0x21, 0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1,
|
||||||
|
0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2,
|
||||||
|
0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74,
|
||||||
|
0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x00, 0xE2, 0x65, 0x75, 0xFF,
|
||||||
|
0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0x62, 0xFF,
|
||||||
|
0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43,
|
||||||
|
0x65, 0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92,
|
||||||
|
0x21, 0x65, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65,
|
||||||
|
0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
||||||
|
0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD,
|
||||||
|
0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0xE7, 0xFF, 0xF6,
|
||||||
|
0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
||||||
|
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
|
0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21,
|
||||||
|
0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC,
|
||||||
|
0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48,
|
||||||
|
0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFF, 0xA8, 0xFF, 0xBF,
|
||||||
|
0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x8D,
|
||||||
|
0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75,
|
||||||
|
0xFD, 0x41, 0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
|
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70,
|
||||||
|
0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||||
|
0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0xFF, 0xFD, 0x42, 0x2E, 0x73,
|
||||||
|
0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x74, 0xFD,
|
||||||
|
0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F,
|
||||||
|
0xE2, 0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21,
|
||||||
|
0x6E, 0xFC, 0x21, 0x65, 0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70,
|
||||||
|
0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE,
|
||||||
|
0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41,
|
||||||
|
0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xE2, 0x2E, 0x62,
|
||||||
|
0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
||||||
|
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D,
|
||||||
|
0xFC, 0xEE, 0xA0, 0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5,
|
||||||
|
0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72,
|
||||||
|
0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD,
|
||||||
|
0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0x72, 0xFC, 0x41, 0x69,
|
||||||
|
0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||||
|
0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4,
|
||||||
|
0x21, 0x69, 0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF,
|
||||||
|
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC,
|
||||||
|
0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7,
|
||||||
|
0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF,
|
||||||
|
0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0x88, 0xA1,
|
||||||
|
0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69,
|
||||||
|
0xFC, 0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE,
|
||||||
|
0x8A, 0xFD, 0x27, 0xFD, 0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF,
|
||||||
|
0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1,
|
||||||
|
0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC,
|
||||||
|
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0xF2, 0x5A, 0xA1,
|
||||||
|
0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
||||||
|
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2,
|
||||||
|
0xF5, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0,
|
||||||
|
0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65,
|
||||||
|
0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1,
|
||||||
|
0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFB, 0x41, 0xFF, 0xFB,
|
||||||
|
0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0xFC,
|
||||||
|
0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73,
|
||||||
|
0xFB, 0x34, 0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69,
|
||||||
|
0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08,
|
||||||
|
0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54,
|
||||||
|
0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xF3,
|
||||||
|
0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0xFF, 0xFC,
|
||||||
|
0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5,
|
||||||
|
0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF,
|
||||||
|
0xFC, 0xFB, 0x62, 0x42, 0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E,
|
||||||
|
0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C,
|
||||||
|
0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD,
|
||||||
|
0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x68, 0xF8, 0xC0,
|
||||||
|
0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
||||||
|
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A,
|
||||||
|
0xC3, 0x69, 0x63, 0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39,
|
||||||
|
0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03,
|
||||||
|
0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A,
|
||||||
|
0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x76,
|
||||||
|
0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0x73,
|
||||||
|
0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D,
|
||||||
|
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2,
|
||||||
|
0x69, 0x75, 0xC3, 0x6F, 0x65, 0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41,
|
||||||
|
0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9,
|
||||||
|
0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFC, 0x3E, 0xFC, 0x3E, 0x41,
|
||||||
|
0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0xFF, 0xFC,
|
||||||
|
0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21,
|
||||||
|
0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C,
|
||||||
|
0xEB, 0xFD, 0x42, 0xA9, 0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||||
|
0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D,
|
||||||
|
0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21,
|
||||||
|
0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0x65, 0xF9, 0x7E,
|
||||||
|
0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
||||||
|
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21,
|
||||||
|
0x75, 0xFC, 0xA0, 0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00,
|
||||||
|
0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF,
|
||||||
|
0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB,
|
||||||
|
0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x07, 0x62, 0x21, 0xA9,
|
||||||
|
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0xFA,
|
||||||
|
0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35,
|
||||||
|
0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6,
|
||||||
|
0x41, 0x75, 0xF8, 0xC2, 0x22, 0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC,
|
||||||
|
0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D,
|
||||||
|
0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x74, 0x79, 0xFE, 0xAE, 0xFE,
|
||||||
|
0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xC2, 0xFF,
|
||||||
|
0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64,
|
||||||
|
0xF1, 0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC,
|
||||||
|
0x41, 0x6C, 0xF1, 0x8F, 0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21,
|
||||||
|
0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42,
|
||||||
|
0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD,
|
||||||
|
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0xB5, 0xBC, 0xCE,
|
||||||
|
0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
||||||
|
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61,
|
||||||
|
0xFC, 0x22, 0x63, 0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD,
|
||||||
|
0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1,
|
||||||
|
0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70,
|
||||||
|
0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x72, 0xF7, 0x21, 0x68,
|
||||||
|
0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFA,
|
||||||
|
0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD,
|
||||||
|
0x41, 0x61, 0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72,
|
||||||
|
0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC,
|
||||||
|
0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69,
|
||||||
|
0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||||
|
0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xFF, 0xA5,
|
||||||
|
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21,
|
||||||
|
0x61, 0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA,
|
||||||
|
0xFF, 0xDF, 0xFF, 0xEB, 0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61,
|
||||||
|
0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63,
|
||||||
|
0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6,
|
||||||
|
0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0x41, 0x70, 0xED,
|
||||||
|
0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
||||||
|
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||||
|
0x6D, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70,
|
||||||
|
0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD,
|
||||||
|
0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21,
|
||||||
|
0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79,
|
||||||
|
0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xFD,
|
||||||
|
0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72,
|
||||||
|
0xF6, 0xA6, 0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74,
|
||||||
|
0xF0, 0x03, 0xFF, 0xFC, 0x45, 0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD,
|
||||||
|
0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61,
|
||||||
|
0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0xFB, 0x9D, 0x21, 0x68, 0xFC,
|
||||||
|
0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xFB, 0xEE,
|
||||||
|
0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41,
|
||||||
|
0x6D, 0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1,
|
||||||
|
0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2,
|
||||||
|
0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21,
|
||||||
|
0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7,
|
||||||
|
0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xFF, 0xF3, 0xF7,
|
||||||
|
0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
||||||
|
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00,
|
||||||
|
0xE1, 0x6D, 0x72, 0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65,
|
||||||
|
0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3,
|
||||||
|
0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5,
|
||||||
|
0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0x41,
|
||||||
|
0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x2E,
|
||||||
|
0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20,
|
||||||
|
0xFF, 0x4D, 0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC,
|
||||||
|
0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||||
|
0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75,
|
||||||
|
0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0x32, 0xFF, 0xFD, 0xC2, 0x00,
|
||||||
|
0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0x65, 0x69,
|
||||||
|
0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD,
|
||||||
|
0xC4, 0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD,
|
||||||
|
0xC4, 0xF4, 0xD1, 0x45, 0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4,
|
||||||
|
0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2,
|
||||||
|
0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0x61, 0xC3, 0x65,
|
||||||
|
0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
||||||
|
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4,
|
||||||
|
0x79, 0x41, 0x69, 0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5,
|
||||||
|
0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5,
|
||||||
|
0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21,
|
||||||
|
0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0x6F, 0x73, 0xF5, 0x12,
|
||||||
|
0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0x70,
|
||||||
|
0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00,
|
||||||
|
0xE2, 0x75, 0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD,
|
||||||
|
0x41, 0x6D, 0xF4, 0x02, 0x21, 0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22,
|
||||||
|
0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0,
|
||||||
|
0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0xFF, 0xFC, 0xF3, 0xDA, 0x41,
|
||||||
|
0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xFB, 0x17,
|
||||||
|
0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6,
|
||||||
|
0x66, 0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29,
|
||||||
|
0xC6, 0x00, 0xE2, 0x2E, 0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF,
|
||||||
|
0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||||
|
0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21,
|
||||||
|
0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA9,
|
||||||
|
0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
||||||
|
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65,
|
||||||
|
0xF3, 0x44, 0x21, 0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9,
|
||||||
|
0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5,
|
||||||
|
0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F,
|
||||||
|
0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x41, 0x69, 0xF3, 0x97,
|
||||||
|
0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0x6E,
|
||||||
|
0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41,
|
||||||
|
0x6F, 0xF6, 0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62,
|
||||||
|
0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F,
|
||||||
|
0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21,
|
||||||
|
0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF7, 0x81, 0x21, 0x72, 0xFC,
|
||||||
|
0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF7, 0x6F,
|
||||||
|
0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||||
|
0x63, 0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC,
|
||||||
|
0xDC, 0xFB, 0x21, 0x74, 0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D,
|
||||||
|
0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D,
|
||||||
|
0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E,
|
||||||
|
0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x72, 0xFF, 0xFB,
|
||||||
|
0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
||||||
|
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1,
|
||||||
|
0x00, 0xE1, 0x6D, 0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB,
|
||||||
|
0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06,
|
||||||
|
0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD,
|
||||||
|
0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
||||||
|
0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x22,
|
||||||
|
0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75,
|
||||||
|
0x79, 0xF1, 0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41,
|
||||||
|
0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC,
|
||||||
|
0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73,
|
||||||
|
0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x6E, 0x73, 0xE9, 0xBA, 0xE9,
|
||||||
|
0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x00, 0xE1,
|
||||||
|
0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41,
|
||||||
|
0x74, 0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF,
|
||||||
|
0xFD, 0xF2, 0x88, 0x42, 0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73,
|
||||||
|
0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD,
|
||||||
|
0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||||
|
0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0x41, 0x6D,
|
||||||
|
0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
||||||
|
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB,
|
||||||
|
0xF2, 0x50, 0xF0, 0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42,
|
||||||
|
0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48,
|
||||||
|
0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6,
|
||||||
|
0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0x21, 0x74, 0xFC, 0x21,
|
||||||
|
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0x6D,
|
||||||
|
0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80,
|
||||||
|
0xF0, 0x80, 0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1,
|
||||||
|
0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0,
|
||||||
|
0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0,
|
||||||
|
0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0x41, 0x69, 0xFA,
|
||||||
|
0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0xFD, 0x22,
|
||||||
|
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69,
|
||||||
|
0x6F, 0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0,
|
||||||
|
0x0B, 0xF4, 0x0D, 0xFF, 0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC,
|
||||||
|
0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61,
|
||||||
|
0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||||
|
0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB,
|
||||||
|
0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
||||||
|
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0,
|
||||||
|
0xEF, 0x48, 0xF0, 0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F,
|
||||||
|
0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4,
|
||||||
|
0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65,
|
||||||
|
0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns fr_patterns = {
|
||||||
|
0x1AF0u,
|
||||||
|
fr_trie_data,
|
||||||
|
sizeof(fr_trie_data),
|
||||||
|
};
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||||
|
0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x0D, 0x16, 0x0B, 0x34,
|
||||||
|
0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x01, 0x02, 0x16, 0x02,
|
||||||
|
0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0B, 0xA0, 0x00, 0x42,
|
||||||
|
0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x6D, 0xFD, 0x21, 0x69,
|
||||||
|
0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
|
0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x6D,
|
||||||
|
0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6F, 0xFD,
|
||||||
|
0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x61, 0x69, 0x6F, 0xDF,
|
||||||
|
0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||||
|
0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x75, 0xFD, 0x21,
|
||||||
|
0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x00,
|
||||||
|
0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0xA0, 0x01, 0x52, 0x21,
|
||||||
|
0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x71, 0x21, 0x6F,
|
||||||
|
0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x6F, 0xFD,
|
||||||
|
0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x72,
|
||||||
|
0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0x6C, 0x72, 0xFD, 0xFD,
|
||||||
|
0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x11, 0x25, 0x61, 0x68,
|
||||||
|
0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x72, 0xFD, 0x21, 0x63,
|
||||||
|
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xA2, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x72, 0xFF, 0xFC, 0xFF,
|
||||||
|
0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x72,
|
||||||
|
0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0x21,
|
||||||
|
0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0xFD, 0x41, 0x6E, 0xFF,
|
||||||
|
0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x61, 0x65,
|
||||||
|
0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x12, 0xFF, 0x20, 0xFF,
|
||||||
|
0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0xC2, 0xFF, 0xE6, 0xFF,
|
||||||
|
0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xA0, 0x00, 0xD1, 0x24,
|
||||||
|
0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0xF1, 0xA0, 0x01,
|
||||||
|
0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xFD, 0x21, 0x75, 0xDF,
|
||||||
|
0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0x02, 0x01, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0xE3, 0xE3, 0xE3, 0xE3,
|
||||||
|
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0x27, 0xC4, 0xC7, 0xC6,
|
||||||
|
0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xFB, 0xFF, 0xBF,
|
||||||
|
0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x6E, 0x71, 0x73,
|
||||||
|
0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||||
|
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xEB, 0xFF,
|
||||||
|
0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0x67, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0x72, 0x73, 0x74, 0x2E,
|
||||||
|
0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x4A, 0xFF,
|
||||||
|
0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0xFD, 0xD1, 0x02, 0x01,
|
||||||
|
0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E,
|
||||||
|
0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x33, 0xFF, 0x21, 0xFF,
|
||||||
|
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
|
||||||
|
0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x68, 0x69, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0xFC, 0xFE, 0xF9, 0xFE,
|
||||||
|
0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0x02, 0x01, 0x2E, 0x27,
|
||||||
|
0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0x6D, 0x72, 0x73, 0x74,
|
||||||
|
0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
|
||||||
|
0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0x2E, 0x27, 0xFE, 0x93,
|
||||||
|
0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||||
|
0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0x21, 0x72, 0xF8, 0x21,
|
||||||
|
0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x69, 0xFC, 0x21, 0x65,
|
||||||
|
0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||||
|
0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||||
|
0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||||
|
0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x82,
|
||||||
|
0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xFD, 0xCB, 0x02, 0x01,
|
||||||
|
0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0xB1, 0xFD, 0xC3, 0xFD,
|
||||||
|
0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD,
|
||||||
|
0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0x8D, 0xA0, 0x02, 0x53,
|
||||||
|
0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x68, 0x67, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x27, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x37, 0xFD, 0x37, 0xFD,
|
||||||
|
0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x8F, 0x4B, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
|
||||||
|
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xA0,
|
||||||
|
0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x70, 0x74, 0x7A, 0x2E,
|
||||||
|
0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0xF8, 0xFF, 0xFB, 0xC1,
|
||||||
|
0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x63, 0xFC, 0xC1, 0x01,
|
||||||
|
0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0x06, 0xD2, 0x02, 0x01,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A,
|
||||||
|
0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xE2, 0xFC, 0xD3,
|
||||||
|
0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1,
|
||||||
|
0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0x76, 0x2E, 0x27, 0xFC,
|
||||||
|
0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x72, 0xFB, 0xAF, 0xA0,
|
||||||
|
0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0xFF, 0xF9, 0xFF, 0xFD,
|
||||||
|
0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0x70, 0x74, 0x77, 0x2E,
|
||||||
|
0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
|
||||||
|
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0xCB, 0x02, 0x01, 0x62,
|
||||||
|
0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
|
||||||
|
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFD, 0x9F,
|
||||||
|
0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0xC2, 0xFB, 0xF9, 0xFC,
|
||||||
|
0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0xC4, 0xFC, 0xED, 0xFD,
|
||||||
|
0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x5D, 0xFE, 0x81, 0xFE,
|
||||||
|
0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xD5, 0xFF, 0xDC,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns it_patterns = {
|
||||||
|
0x5C0u,
|
||||||
|
it_trie_data,
|
||||||
|
sizeof(it_trie_data),
|
||||||
|
};
|
||||||
@@ -0,0 +1,987 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t pl_trie_data[] = {
|
||||||
|
0x3A, 0x26, 0x30, 0x48, 0x17, 0x3E, 0x0F, 0x52, 0x67, 0x0C, 0x44, 0x16, 0x0C, 0x0D, 0x16, 0x0D,
|
||||||
|
0x22, 0x23, 0x21, 0x35, 0x16, 0x35, 0x0C, 0x21, 0x0C, 0x5C, 0x0D, 0x1C, 0x20, 0x0D, 0x21, 0x0E,
|
||||||
|
0x34, 0x34, 0x0B, 0x3F, 0x16, 0x3F, 0x0C, 0x49, 0x0C, 0x53, 0x16, 0x70, 0x0D, 0x49, 0x16, 0x3E,
|
||||||
|
0x0D, 0x3F, 0x0E, 0x21, 0x0E, 0x0D, 0x21, 0x16, 0x22, 0x17, 0x22, 0x0D, 0x23, 0x0E, 0x0C, 0x34,
|
||||||
|
0x0D, 0x35, 0x0E, 0x7B, 0x16, 0x84, 0x0D, 0x5C, 0x17, 0x16, 0x0D, 0x0C, 0x0F, 0x16, 0x16, 0x0B,
|
||||||
|
0x0C, 0x0B, 0x2C, 0x17, 0x0E, 0x18, 0x0F, 0x0C, 0x2B, 0x0C, 0x52, 0x0D, 0x66, 0x2A, 0x0C, 0x0D,
|
||||||
|
0x2B, 0x18, 0x2B, 0x18, 0x0D, 0x2C, 0x19, 0x0C, 0x53, 0x0C, 0x2A, 0x17, 0x20, 0x0C, 0x21, 0x20,
|
||||||
|
0x21, 0x0E, 0x0C, 0x22, 0x0D, 0x0C, 0x0C, 0x22, 0x0F, 0x5F, 0x2B, 0x16, 0x2D, 0x16, 0x2D, 0x0C,
|
||||||
|
0x36, 0x0D, 0x2B, 0x0C, 0x16, 0x2B, 0x0C, 0x0C, 0x2B, 0x16, 0x0D, 0x41, 0x16, 0x34, 0x0C, 0x2C,
|
||||||
|
0x0D, 0x2C, 0x21, 0x3E, 0x0C, 0x3F, 0x20, 0x40, 0x0D, 0x0C, 0x4B, 0x52, 0x0C, 0x53, 0x20, 0x53,
|
||||||
|
0x0E, 0x53, 0x0E, 0x16, 0x53, 0x0C, 0x0C, 0x0D, 0x54, 0x0F, 0x54, 0x0F, 0x16, 0x54, 0x0F, 0x0C,
|
||||||
|
0x5D, 0x16, 0x5D, 0x0C, 0x7A, 0x17, 0x0E, 0x16, 0x17, 0x10, 0x17, 0x0C, 0x18, 0x11, 0x21, 0x0C,
|
||||||
|
0x0C, 0x2B, 0x0D, 0x23, 0x16, 0x23, 0x0C, 0x2A, 0x0D, 0x20, 0x16, 0x22, 0x0F, 0x0C, 0x2A, 0x16,
|
||||||
|
0x34, 0x0B, 0x20, 0x34, 0x17, 0x21, 0x0C, 0x0C, 0x0D, 0x22, 0x0D, 0x0E, 0x0C, 0x0D, 0x22, 0x0F,
|
||||||
|
0x16, 0x22, 0x17, 0x0C, 0x2B, 0x34, 0x0F, 0x35, 0x20, 0x2C, 0x0B, 0x2C, 0x0C, 0x35, 0x0E, 0x0D,
|
||||||
|
0x35, 0x0E, 0x16, 0x35, 0x0C, 0x0C, 0x0D, 0x36, 0x0F, 0x36, 0x0F, 0x16, 0x36, 0x0F, 0x0E, 0x48,
|
||||||
|
0x0D, 0x49, 0x0E, 0x3F, 0x2B, 0x0C, 0x21, 0x0E, 0x0D, 0x16, 0x21, 0x0E, 0x0D, 0x0C, 0x22, 0x0F,
|
||||||
|
0x0E, 0x0D, 0x36, 0x0F, 0x0C, 0x41, 0x0C, 0x48, 0x0B, 0x66, 0x0D, 0x2A, 0x16, 0x17, 0x20, 0x11,
|
||||||
|
0x4A, 0x0F, 0x18, 0x0D, 0x0C, 0x18, 0x17, 0x22, 0x0D, 0x20, 0x48, 0x21, 0x3E, 0x17, 0x21, 0x0C,
|
||||||
|
0x0D, 0x68, 0x34, 0x2B, 0x2B, 0x0E, 0x22, 0x0D, 0x0C, 0x0B, 0x0C, 0x52, 0x17, 0x36, 0x21, 0x15,
|
||||||
|
0x01, 0x16, 0x02, 0x15, 0x02, 0x0B, 0x02, 0x1F, 0x03, 0x0C, 0x03, 0x0C, 0x16, 0x03, 0x0C, 0x0C,
|
||||||
|
0x04, 0x17, 0x04, 0x0D, 0x08, 0x16, 0x0D, 0x16, 0x16, 0x0D, 0x16, 0x0D, 0x3E, 0x0B, 0x20, 0x0B,
|
||||||
|
0x20, 0x0C, 0x0D, 0x20, 0x17, 0x29, 0x16, 0x11, 0x16, 0x0F, 0x20, 0x0B, 0x0C, 0x2A, 0x0B, 0x16,
|
||||||
|
0x15, 0x18, 0x0E, 0x0D, 0x20, 0x0F, 0x16, 0x11, 0x3F, 0x18, 0x16, 0x0D, 0x16, 0x16, 0x0D, 0x17,
|
||||||
|
0x0C, 0x20, 0x15, 0x17, 0x0C, 0x0D, 0x16, 0x0C, 0x0B, 0xA0, 0x00, 0x41, 0x21, 0x87, 0xFD, 0x25,
|
||||||
|
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xA0, 0x00, 0x61, 0xA0, 0x00, 0x72,
|
||||||
|
0x21, 0x87, 0xFD, 0x21, 0xC4, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x61, 0x69,
|
||||||
|
0xFD, 0xB5, 0x00, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xDB, 0xDE, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9,
|
||||||
|
0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xFB, 0xE9, 0xE9, 0x21, 0x87,
|
||||||
|
0xD3, 0xD5, 0x00, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFF, 0xAB, 0xFF, 0xAE, 0xFF, 0xB9, 0xFF,
|
||||||
|
0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF,
|
||||||
|
0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF,
|
||||||
|
0xB9, 0xFF, 0xB9, 0xA0, 0x00, 0xB1, 0xA1, 0x00, 0x92, 0x72, 0xFD, 0x21, 0x64, 0xFB, 0x21, 0xB3,
|
||||||
|
0xFD, 0xA1, 0x00, 0x61, 0xC3, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x77, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
|
0x82, 0xFD, 0x21, 0xC5, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x00, 0x61, 0x69, 0xFD,
|
||||||
|
0xD5, 0x00, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFF, 0x3C, 0xFF, 0x3F, 0xFF, 0x4A, 0xFF, 0x4A,
|
||||||
|
0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A,
|
||||||
|
0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0xE1, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0xFB, 0xFF, 0x4A,
|
||||||
|
0xFF, 0x4A, 0xA0, 0x00, 0xE1, 0x21, 0xBA, 0xFD, 0xA1, 0x00, 0x61, 0xC5, 0xFD, 0xD5, 0x00, 0x51,
|
||||||
|
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x76, 0x77, 0x78, 0x7A, 0xFE, 0xEF, 0xFE, 0xF2, 0xFE, 0xFD, 0xFE, 0xFD, 0xFF, 0xFB, 0xFE,
|
||||||
|
0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE,
|
||||||
|
0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0x45,
|
||||||
|
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0x02, 0xFF, 0x02, 0xFF, 0x71, 0xFF, 0xBE, 0xFF, 0x02, 0xA0,
|
||||||
|
0x00, 0xF3, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x51, 0x21, 0x74, 0xFD, 0xA1, 0x01,
|
||||||
|
0x41, 0x70, 0xFD, 0xA0, 0x01, 0x41, 0xA1, 0x01, 0x41, 0x75, 0xF2, 0xA2, 0x01, 0x41, 0x70, 0x72,
|
||||||
|
0xED, 0xED, 0xA6, 0x01, 0x22, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xEC, 0xF1, 0xF4, 0xF9, 0xF1,
|
||||||
|
0xF1, 0xA0, 0x00, 0x91, 0xA5, 0x01, 0x61, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0x21, 0x6F, 0xF3, 0x21, 0x72, 0xFD, 0x25, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xEA, 0xEA, 0xEA,
|
||||||
|
0xEA, 0xEA, 0x21, 0x79, 0xF5, 0x21, 0x74, 0xFD, 0xA0, 0x01, 0x72, 0x21, 0x82, 0xFD, 0xA1, 0x01,
|
||||||
|
0x92, 0x7A, 0xFA, 0xA0, 0x01, 0x92, 0x29, 0xC5, 0x62, 0x6B, 0x6D, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||||
|
0xF5, 0xF8, 0xFD, 0xFD, 0xCB, 0xCB, 0xCB, 0xCB, 0xCB, 0x21, 0x79, 0xED, 0x21, 0x63, 0xFD, 0xA0,
|
||||||
|
0x01, 0xB2, 0x21, 0x68, 0xD6, 0xA0, 0x01, 0xD2, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61,
|
||||||
|
0xFD, 0x21, 0x72, 0xFD, 0x47, 0x63, 0x61, 0x65, 0x69, 0x6F, 0x74, 0x75, 0xFF, 0xEE, 0xFF, 0x9D,
|
||||||
|
0xFF, 0x9D, 0xFF, 0x9D, 0xFF, 0x9D, 0xFF, 0xFD, 0xFF, 0x9D, 0xA1, 0x01, 0xB2, 0x6F, 0xEA, 0x23,
|
||||||
|
0x67, 0x6B, 0x74, 0xD0, 0xD0, 0xFB, 0x46, 0x62, 0x64, 0x65, 0x6E, 0x72, 0x75, 0xFF, 0x4F, 0xFF,
|
||||||
|
0x6C, 0xFF, 0x8E, 0xFF, 0x9F, 0xFF, 0xC6, 0xFF, 0xF9, 0x41, 0x87, 0xFD, 0xE1, 0x45, 0x82, 0x84,
|
||||||
|
0x9B, 0xBA, 0xBC, 0xFD, 0xDD, 0xFD, 0xDD, 0xFD, 0xDD, 0xFD, 0xDD, 0xFD, 0xDD, 0xA0, 0x00, 0x51,
|
||||||
|
0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x63, 0xFA, 0x21, 0x6B, 0xF7, 0x41, 0x68, 0xFF, 0x45,
|
||||||
|
0xA0, 0x02, 0x41, 0xA0, 0x02, 0x52, 0x21, 0x6A, 0xFD, 0xA1, 0x02, 0x41, 0x62, 0xFD, 0x41, 0x77,
|
||||||
|
0xFF, 0x33, 0xA8, 0x02, 0x02, 0x61, 0x65, 0x69, 0x63, 0x6D, 0x6F, 0x77, 0x7A, 0xE1, 0xE4, 0xE7,
|
||||||
|
0xEA, 0xEE, 0xF7, 0xEE, 0xFC, 0x21, 0x7A, 0xED, 0xC1, 0x00, 0x51, 0x7A, 0xFD, 0x92, 0xB6, 0x01,
|
||||||
|
0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70,
|
||||||
|
0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xAB, 0xAF, 0xBF, 0xBF, 0xBF, 0xF7, 0xBF, 0xBF, 0xBF,
|
||||||
|
0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xFA, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xA0, 0x02, 0x72,
|
||||||
|
0x21, 0x9B, 0xFD, 0xA0, 0x02, 0x92, 0x22, 0xC5, 0x6B, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x82,
|
||||||
|
0xFD, 0x21, 0xC5, 0xFD, 0x41, 0x7A, 0xFD, 0x35, 0xA1, 0x00, 0x51, 0x72, 0xFC, 0xA0, 0x02, 0xB2,
|
||||||
|
0x21, 0x77, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6B, 0xFD, 0xA0, 0x02, 0xD2, 0x21, 0x72, 0xFD, 0x21,
|
||||||
|
0x6B, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x82, 0xFD, 0x21, 0xC5, 0xFD, 0x22, 0x6E, 0x70, 0xEB, 0xFD,
|
||||||
|
0x21, 0x65, 0xFB, 0x21, 0x6B, 0xDA, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x72, 0xFD, 0xA0,
|
||||||
|
0x02, 0xF2, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x72, 0x73, 0xFD, 0x21, 0x68, 0xFB,
|
||||||
|
0x21, 0x63, 0xFD, 0xA0, 0x03, 0x12, 0x21, 0x9B, 0xFD, 0x21, 0xC5, 0xFD, 0x22, 0x65, 0x6F, 0xF4,
|
||||||
|
0xFD, 0x21, 0x72, 0xFB, 0x21, 0x65, 0xFD, 0xA0, 0x03, 0x52, 0x22, 0x85, 0x99, 0xFD, 0xFD, 0xA4,
|
||||||
|
0x03, 0x32, 0xC4, 0x61, 0x65, 0x6F, 0xFB, 0xF8, 0xF8, 0xF8, 0x21, 0x72, 0xF5, 0x21, 0xB3, 0xFD,
|
||||||
|
0x21, 0xC3, 0xFD, 0xC4, 0x00, 0x51, 0x61, 0x6B, 0x74, 0x77, 0xFF, 0xB9, 0xFC, 0xC7, 0xFF, 0xE1,
|
||||||
|
0xFF, 0xFD, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFE, 0xD7, 0xFE, 0xDB,
|
||||||
|
0xFF, 0x6F, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFF, 0x76, 0xFF, 0x9E,
|
||||||
|
0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB,
|
||||||
|
0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFF, 0xF1, 0x41, 0x72, 0xFF, 0x79, 0x42, 0x74,
|
||||||
|
0x77, 0xFF, 0xFC, 0xFF, 0x2F, 0x21, 0x6F, 0xF9, 0x21, 0x67, 0xFD, 0xA1, 0x00, 0x61, 0x75, 0xFD,
|
||||||
|
0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0xFB, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A,
|
||||||
|
0x41, 0x65, 0xFF, 0x16, 0x21, 0x6C, 0xFC, 0xA0, 0x03, 0x73, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||||
|
0x21, 0x75, 0xF7, 0xA2, 0x02, 0x41, 0x62, 0x77, 0xFA, 0xFD, 0x42, 0x6C, 0x72, 0xFE, 0x66, 0xFE,
|
||||||
|
0x66, 0xC4, 0x02, 0x02, 0x61, 0x65, 0x79, 0x6F, 0xFF, 0xF2, 0xFF, 0xF9, 0xFE, 0x5F, 0xFE, 0x6F,
|
||||||
|
0x21, 0x7A, 0xF1, 0xA0, 0x03, 0xA2, 0x21, 0x87, 0xFD, 0xA0, 0x03, 0xC2, 0xA1, 0x03, 0xA2, 0x6B,
|
||||||
|
0xFD, 0x24, 0x82, 0x9B, 0xBA, 0xBC, 0xFB, 0xF2, 0xF2, 0xF2, 0xC1, 0x02, 0x41, 0x7A, 0xFD, 0x87,
|
||||||
|
0xA1, 0x01, 0xB2, 0x72, 0xFA, 0xC2, 0x01, 0xB2, 0x68, 0x7A, 0xFE, 0x3B, 0xFE, 0x3B, 0x42, 0xBA,
|
||||||
|
0xBC, 0xFD, 0x73, 0xFD, 0x73, 0xC2, 0x01, 0xB2, 0xC5, 0x7A, 0xFF, 0xF9, 0xFE, 0x2B, 0x41, 0x7A,
|
||||||
|
0xFD, 0x63, 0xA1, 0x01, 0xB2, 0x72, 0xFC, 0xA0, 0x03, 0xE2, 0xA1, 0x01, 0xB2, 0x74, 0xFD, 0xA1,
|
||||||
|
0x01, 0xB2, 0x6E, 0xF8, 0xA0, 0x04, 0x03, 0xA1, 0x03, 0xE2, 0x6E, 0xFD, 0xA1, 0x01, 0xB2, 0x6B,
|
||||||
|
0xFB, 0xC2, 0x01, 0xB2, 0x63, 0x72, 0xFD, 0xFB, 0xFF, 0xDD, 0x41, 0xBC, 0xFD, 0x37, 0xC3, 0x01,
|
||||||
|
0xB2, 0x73, 0xC5, 0x7A, 0xFF, 0xD9, 0xFF, 0xFC, 0xFD, 0xF2, 0xC3, 0x01, 0xB2, 0x63, 0x6D, 0x7A,
|
||||||
|
0xFD, 0xE2, 0xFD, 0xE6, 0xFD, 0xE6, 0xC2, 0x01, 0xB2, 0x6B, 0x72, 0xFD, 0xDA, 0xFD, 0xDA, 0xA1,
|
||||||
|
0x01, 0xB2, 0x63, 0xB8, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0x72, 0xFF, 0x7D, 0xFF, 0x8C, 0xFF, 0x91, 0xFF, 0xA1,
|
||||||
|
0xFD, 0x5B, 0xFF, 0xAE, 0xFD, 0x5B, 0xFF, 0xB6, 0xFF, 0xBB, 0xFF, 0xC8, 0xFF, 0xCD, 0xFF, 0xDA,
|
||||||
|
0xFF, 0xE6, 0xFF, 0xF2, 0xFF, 0xFB, 0xFD, 0x5B, 0x41, 0x77, 0xFD, 0xFB, 0x21, 0x6F, 0xFC, 0x21,
|
||||||
|
0x67, 0xFD, 0xC2, 0x00, 0x51, 0x6F, 0x7A, 0xFF, 0xFD, 0xFB, 0x48, 0xA0, 0x04, 0x52, 0x22, 0x85,
|
||||||
|
0x99, 0xFD, 0xFD, 0xA4, 0x04, 0x32, 0xC4, 0x61, 0x65, 0x6F, 0xFB, 0xF8, 0xF8, 0xF8, 0x21, 0x6A,
|
||||||
|
0xF5, 0x21, 0xB3, 0xFD, 0xA1, 0x00, 0x51, 0xC3, 0xFD, 0x41, 0x61, 0xFD, 0x57, 0xA0, 0x01, 0x61,
|
||||||
|
0xC5, 0x02, 0x02, 0x65, 0x6F, 0x74, 0x79, 0x7A, 0xFD, 0x50, 0xFD, 0x50, 0xFF, 0xF9, 0xFD, 0x50,
|
||||||
|
0xFF, 0xFD, 0xC2, 0x02, 0x02, 0x65, 0x75, 0xFD, 0x3E, 0xFD, 0x4E, 0x22, 0x73, 0x7A, 0xE5, 0xF7,
|
||||||
|
0xA0, 0x04, 0x72, 0x21, 0x9B, 0xFD, 0x21, 0xC5, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x21,
|
||||||
|
0x63, 0xFD, 0x21, 0x99, 0xFD, 0x21, 0xC4, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x04, 0x92, 0x21, 0x74,
|
||||||
|
0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x04, 0xB2, 0x73, 0xFD, 0x21, 0x87, 0xFB, 0x22, 0xC4, 0x63, 0xFD,
|
||||||
|
0xE0, 0x21, 0x99, 0xFB, 0x21, 0xC4, 0xFD, 0x21, 0x69, 0xFD, 0x22, 0x73, 0x77, 0xDE, 0xFD, 0x21,
|
||||||
|
0x65, 0xFB, 0xA1, 0x00, 0x51, 0x69, 0xFD, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64,
|
||||||
|
0x65, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||||
|
0x78, 0x79, 0x7A, 0xFC, 0xD2, 0xFE, 0x59, 0xFE, 0x6D, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFE,
|
||||||
|
0x99, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC,
|
||||||
|
0xE6, 0xFF, 0x2D, 0xFC, 0xE6, 0xFF, 0x6B, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFF, 0x8D, 0xFC,
|
||||||
|
0xE6, 0xFF, 0xB4, 0xFF, 0xFB, 0xA0, 0x05, 0x61, 0xA1, 0x00, 0xF3, 0x79, 0xFD, 0x21, 0x73, 0xFB,
|
||||||
|
0xA0, 0x04, 0xD5, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||||
|
0x65, 0xFD, 0xA0, 0x05, 0x24, 0x21, 0x61, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x05,
|
||||||
|
0x22, 0x21, 0x61, 0xFD, 0x23, 0x65, 0x68, 0x74, 0xF7, 0xFD, 0xFA, 0x45, 0x61, 0x65, 0x69, 0x6F,
|
||||||
|
0x75, 0xFC, 0x75, 0xFC, 0x75, 0xFC, 0x75, 0xFC, 0x75, 0xFC, 0x75, 0x21, 0x6F, 0xF0, 0xA0, 0x01,
|
||||||
|
0xD1, 0x25, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF5, 0x21,
|
||||||
|
0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xFD, 0x45, 0x6B, 0x6D, 0x73, 0x67,
|
||||||
|
0x6C, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xC9, 0xFF, 0xE0, 0xFF, 0xFD, 0xD5, 0x01, 0xF1, 0xC4, 0xC5,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76,
|
||||||
|
0x77, 0x78, 0x7A, 0xFC, 0x0E, 0xFC, 0x12, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC,
|
||||||
|
0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC,
|
||||||
|
0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xA0, 0x02, 0x02,
|
||||||
|
0x21, 0x6F, 0xFD, 0x21, 0x75, 0xFA, 0x41, 0x62, 0xFE, 0x56, 0xC2, 0x00, 0x51, 0x75, 0x7A, 0xFF,
|
||||||
|
0xFC, 0xF9, 0xA0, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFB, 0xB6, 0xFB,
|
||||||
|
0xBA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFF, 0xED, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB,
|
||||||
|
0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFF, 0xF0, 0xFB, 0xCA, 0xFF, 0xF7, 0xFB,
|
||||||
|
0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xA0, 0x00, 0x71, 0xC3, 0x04,
|
||||||
|
0x32, 0x6F, 0x61, 0x65, 0xFE, 0x0D, 0xFF, 0xFD, 0xFF, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x70, 0xFD, 0xD6, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFB, 0x56, 0xFB, 0x5A,
|
||||||
|
0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFF, 0xFD, 0xFB, 0x6A,
|
||||||
|
0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A,
|
||||||
|
0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xA0, 0x05, 0x72, 0x21, 0x63, 0xFD, 0xA0, 0x05,
|
||||||
|
0x93, 0xA0, 0x05, 0xC2, 0xA2, 0x05, 0x72, 0x6B, 0x77, 0xFA, 0xFD, 0x41, 0x6E, 0xFD, 0x0C, 0x21,
|
||||||
|
0x61, 0xFC, 0x21, 0x6C, 0xFD, 0xA4, 0x01, 0x22, 0x69, 0x6F, 0x75, 0x66, 0xE6, 0xEF, 0xE3, 0xFD,
|
||||||
|
0xA0, 0x01, 0x22, 0x21, 0x6C, 0xFD, 0x43, 0x6E, 0x73, 0x7A, 0xFF, 0xEF, 0xFF, 0xFD, 0xFE, 0xA5,
|
||||||
|
0x41, 0x82, 0xFB, 0x83, 0x21, 0xC5, 0xFC, 0x21, 0x64, 0xFD, 0xD6, 0x01, 0xF1, 0xC4, 0xC5, 0x61,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76,
|
||||||
|
0x77, 0x78, 0x7A, 0xFA, 0xCF, 0xFA, 0xD3, 0xFF, 0xFD, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA,
|
||||||
|
0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA,
|
||||||
|
0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xA0,
|
||||||
|
0x06, 0x01, 0xA1, 0x05, 0xE2, 0x6F, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD,
|
||||||
|
0x21, 0x75, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x82, 0xFD, 0x03, 0xA0, 0x06, 0x93,
|
||||||
|
0x21, 0x69, 0xFD, 0x22, 0x65, 0x79, 0xFA, 0xFA, 0x21, 0x61, 0xF5, 0xA0, 0x06, 0x92, 0x21, 0x6D,
|
||||||
|
0xFD, 0x21, 0x63, 0xFA, 0x21, 0x74, 0xF7, 0x21, 0x67, 0xF4, 0xA7, 0x06, 0x42, 0x67, 0x73, 0x74,
|
||||||
|
0x64, 0x6B, 0x6C, 0x72, 0xE6, 0xE9, 0xEE, 0xF4, 0xF7, 0xFA, 0xFD, 0xA0, 0x06, 0x42, 0xA0, 0x06,
|
||||||
|
0x63, 0xA2, 0x00, 0x71, 0x6C, 0x77, 0xFD, 0xFD, 0xC5, 0x06, 0x13, 0x61, 0x65, 0x6F, 0x79, 0x75,
|
||||||
|
0xFF, 0xE2, 0xFF, 0xF3, 0xFF, 0xF9, 0xFF, 0xF3, 0xFE, 0xC3, 0x21, 0x72, 0xEE, 0x21, 0x74, 0xFD,
|
||||||
|
0x42, 0xC5, 0x6E, 0xFF, 0xA9, 0xFF, 0xFD, 0xA0, 0x06, 0xC2, 0x42, 0x74, 0x77, 0xFA, 0xC2, 0xFF,
|
||||||
|
0xFD, 0x21, 0x6F, 0xF9, 0x21, 0x6B, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xB3, 0xFD, 0xA0, 0x06, 0xE2,
|
||||||
|
0x21, 0x87, 0xFD, 0x21, 0xC4, 0xFD, 0xC3, 0x00, 0x51, 0xC3, 0x6F, 0x7A, 0xFF, 0xF4, 0xFF, 0xFD,
|
||||||
|
0xF7, 0xD4, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B,
|
||||||
|
0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xF9, 0xE7, 0xF9, 0xEB,
|
||||||
|
0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xFF, 0x74, 0xF9, 0xFB,
|
||||||
|
0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xFF, 0xCE, 0xF9, 0xFB, 0xFF, 0xF4, 0xF9, 0xFB,
|
||||||
|
0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0x41, 0x77, 0xF9, 0x39, 0x21, 0x6F,
|
||||||
|
0xFC, 0x21, 0x64, 0xFD, 0xD6, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x7A, 0xF9, 0x95, 0xF9,
|
||||||
|
0x99, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9,
|
||||||
|
0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xFF,
|
||||||
|
0xFD, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xA0, 0x01, 0x71, 0x21, 0x70, 0xFD, 0x21,
|
||||||
|
0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x6E, 0xF9, 0xE8, 0x21,
|
||||||
|
0xBC, 0xFC, 0x21, 0xC5, 0xFD, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||||
|
0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xF9,
|
||||||
|
0x34, 0xF9, 0x38, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xFF,
|
||||||
|
0xF3, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xFF, 0xFD, 0xF9, 0x48, 0xF9,
|
||||||
|
0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xA0, 0x07, 0xE2,
|
||||||
|
0x43, 0x85, 0x99, 0x87, 0xFB, 0xAD, 0xFB, 0xAD, 0xFF, 0xFD, 0xA0, 0x07, 0x22, 0x41, 0x62, 0xF8,
|
||||||
|
0xF3, 0xA1, 0x07, 0xE2, 0x75, 0xFC, 0xA0, 0x07, 0xD1, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||||
|
0x69, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x07, 0xE2, 0x72, 0xFD,
|
||||||
|
0xA0, 0x08, 0x02, 0x24, 0xBA, 0x82, 0x9B, 0xBC, 0xD7, 0xDE, 0xF8, 0xFD, 0xC1, 0x05, 0xC2, 0x72,
|
||||||
|
0xF8, 0xC4, 0x41, 0x68, 0xF8, 0xBE, 0x21, 0x63, 0xFC, 0xA1, 0x05, 0xC2, 0x75, 0xFD, 0x41, 0x7A,
|
||||||
|
0xF8, 0xB2, 0x21, 0x63, 0xFC, 0x21, 0x99, 0xFD, 0xA0, 0x07, 0x43, 0x42, 0x63, 0x74, 0xFF, 0xF3,
|
||||||
|
0xF8, 0xA5, 0x21, 0x70, 0xF9, 0xA0, 0x07, 0xB2, 0x22, 0x85, 0x99, 0xFD, 0xFD, 0x21, 0x7A, 0xF8,
|
||||||
|
0x21, 0x63, 0xFD, 0xA3, 0x02, 0x22, 0xC4, 0x65, 0x79, 0xF5, 0xFD, 0xF2, 0xC5, 0x05, 0xC2, 0xC4,
|
||||||
|
0x77, 0x65, 0x75, 0x7A, 0xFF, 0xD9, 0xFF, 0xDC, 0xFF, 0xE6, 0xF8, 0x8D, 0xFF, 0xF7, 0x41, 0x72,
|
||||||
|
0xF8, 0x75, 0xA1, 0x05, 0xC2, 0x6F, 0xFC, 0x41, 0x6A, 0xFB, 0x16, 0x41, 0x74, 0xFD, 0x46, 0xA1,
|
||||||
|
0x01, 0x92, 0x61, 0xFC, 0x41, 0x87, 0xF7, 0xD4, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF7, 0xD0, 0xF7,
|
||||||
|
0xD0, 0xF7, 0xD0, 0xF7, 0xD0, 0xC2, 0x01, 0x92, 0x68, 0x7A, 0xFC, 0xC6, 0xFC, 0xC6, 0x42, 0xBA,
|
||||||
|
0xBC, 0xF6, 0x95, 0xF6, 0x95, 0xC2, 0x01, 0x92, 0xC5, 0x7A, 0xFF, 0xF9, 0xFC, 0xB6, 0xA0, 0x08,
|
||||||
|
0x42, 0xA3, 0x01, 0x92, 0x63, 0x6E, 0x74, 0xFD, 0xFD, 0xFD, 0x41, 0xBC, 0xF6, 0x79, 0xC2, 0x01,
|
||||||
|
0x92, 0xC5, 0x7A, 0xFF, 0xFC, 0xFC, 0x9D, 0xC1, 0x01, 0x92, 0x7A, 0xFC, 0x94, 0xD1, 0x01, 0x61,
|
||||||
|
0x74, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x77,
|
||||||
|
0x7A, 0xFF, 0xB2, 0xFF, 0xB7, 0xFF, 0xBB, 0xF7, 0x96, 0xFF, 0xC8, 0xFF, 0xD8, 0xF7, 0x96, 0xF7,
|
||||||
|
0x96, 0xF7, 0x96, 0xF7, 0x96, 0xF7, 0x96, 0xF7, 0x96, 0xFF, 0xE4, 0xFF, 0xF1, 0xFF, 0xFA, 0xF7,
|
||||||
|
0x96, 0xF7, 0x96, 0xA0, 0x07, 0x74, 0x21, 0x82, 0xFD, 0xA0, 0x07, 0x73, 0x21, 0x7A, 0xFD, 0x21,
|
||||||
|
0x63, 0xFD, 0x21, 0x69, 0xF7, 0x22, 0x6A, 0x77, 0xFA, 0xFD, 0x21, 0x74, 0xEF, 0x21, 0x6F, 0xFD,
|
||||||
|
0x23, 0xC5, 0x6F, 0x72, 0xE6, 0xF5, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x6E, 0xDF, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0x41, 0x7A, 0xF9, 0xA4, 0x21, 0x72, 0xFC, 0x21, 0x62, 0xFD, 0xA4, 0x01, 0x61,
|
||||||
|
0x62, 0x64, 0x6B, 0x6C, 0xE3, 0xEA, 0xF3, 0xFD, 0x41, 0x6D, 0xFE, 0xFD, 0xA1, 0x01, 0x61, 0x65,
|
||||||
|
0xFC, 0xA0, 0x08, 0x01, 0x21, 0xB3, 0xFD, 0xA0, 0x08, 0x22, 0x21, 0x73, 0xFD, 0x21, 0x79, 0xFD,
|
||||||
|
0x21, 0x7A, 0xF1, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0xA5, 0x02, 0x41, 0x69, 0xC3, 0x6D, 0x6F,
|
||||||
|
0x77, 0xE3, 0xEB, 0xF4, 0xE8, 0xFD, 0xC2, 0x05, 0xC2, 0x68, 0x7A, 0xF6, 0xBB, 0xF6, 0xBB, 0x41,
|
||||||
|
0xBA, 0xFB, 0xDC, 0xA1, 0x05, 0xC2, 0xC5, 0xFC, 0xC1, 0x05, 0xC2, 0x7A, 0xF6, 0xA9, 0xC1, 0x05,
|
||||||
|
0xC2, 0x72, 0xF6, 0xA3, 0xD9, 0x07, 0x02, 0xC4, 0xC5, 0x69, 0x6D, 0x72, 0x77, 0x61, 0x65, 0x6F,
|
||||||
|
0x79, 0x7A, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6E, 0x70, 0x73, 0x74, 0x75,
|
||||||
|
0xFE, 0x4C, 0xFE, 0x7F, 0xFE, 0x88, 0xFE, 0x95, 0xFE, 0xC8, 0xFE, 0xDE, 0xFE, 0xE3, 0xFF, 0x39,
|
||||||
|
0xFF, 0xA9, 0xF9, 0xF9, 0xFF, 0xD5, 0xFC, 0x2D, 0xFF, 0xE2, 0xFF, 0xEF, 0xFC, 0x2D, 0xFC, 0x2D,
|
||||||
|
0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFF, 0xF4, 0xFF, 0xFA,
|
||||||
|
0xFC, 0x2D, 0x41, 0x64, 0xF9, 0xAB, 0xA0, 0x05, 0xC1, 0x21, 0x74, 0xFD, 0x23, 0x7A, 0x6B, 0x75,
|
||||||
|
0xF6, 0xFD, 0xFA, 0x42, 0x73, 0x6F, 0xFF, 0x10, 0xFF, 0x16, 0x41, 0x66, 0xF8, 0xDD, 0xC9, 0x01,
|
||||||
|
0x61, 0x66, 0x67, 0x6B, 0x6E, 0x72, 0x73, 0x77, 0x6D, 0x75, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0xF5,
|
||||||
|
0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0xFC, 0xF8, 0xD9, 0x44, 0x82, 0x9B, 0xBA,
|
||||||
|
0xBC, 0xFD, 0xC1, 0xFD, 0xC1, 0xFD, 0xC1, 0xFD, 0xC1, 0xC1, 0x03, 0x32, 0x77, 0xF5, 0x0A, 0x21,
|
||||||
|
0x7A, 0xFA, 0xA1, 0x05, 0xC2, 0x65, 0xFD, 0x42, 0xBA, 0xBC, 0xFB, 0x24, 0xFB, 0x24, 0x41, 0x87,
|
||||||
|
0xF7, 0x0F, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF7, 0x0B, 0xF7, 0x0B, 0xF7, 0x0B, 0xF7, 0x0B, 0xC2,
|
||||||
|
0x02, 0x92, 0x68, 0x7A, 0xF4, 0xE4, 0xF4, 0xE4, 0x42, 0xBA, 0xBC, 0xFA, 0x36, 0xFA, 0x36, 0xC2,
|
||||||
|
0x02, 0x92, 0xC5, 0x7A, 0xFF, 0xF9, 0xF4, 0xD4, 0xC1, 0x02, 0x92, 0x7A, 0xF4, 0xCB, 0xC1, 0x02,
|
||||||
|
0x92, 0x6B, 0xF4, 0xC5, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xCA, 0xFF, 0xCE, 0xF6, 0xDF, 0xFF, 0xDB, 0xFF, 0xEB,
|
||||||
|
0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xFF, 0xF4,
|
||||||
|
0xFF, 0xF4, 0xFF, 0xFA, 0xF6, 0xDF, 0xF6, 0xDF, 0xC3, 0x05, 0xC2, 0xC5, 0x6F, 0x7A, 0xFF, 0x8F,
|
||||||
|
0xFF, 0xCC, 0xF5, 0x89, 0xA0, 0x08, 0x63, 0x21, 0x87, 0xFD, 0x24, 0x82, 0x9B, 0xBA, 0xBC, 0xFA,
|
||||||
|
0xFA, 0xFA, 0xFA, 0x41, 0x7A, 0xF4, 0x70, 0xD1, 0x08, 0x93, 0xC4, 0xC5, 0x63, 0x64, 0x66, 0x67,
|
||||||
|
0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x72, 0xFF, 0x77, 0xFF, 0x7B, 0xFF,
|
||||||
|
0x88, 0xFF, 0x98, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6,
|
||||||
|
0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xFF, 0xA1, 0xF6, 0x8C, 0xF6, 0x8C, 0xFF, 0xFC, 0xC2, 0x08, 0x93,
|
||||||
|
0x68, 0x7A, 0xFA, 0x5E, 0xFA, 0x5E, 0xA0, 0x08, 0xF2, 0x43, 0xBA, 0x9B, 0xBC, 0xF4, 0x2A, 0xF6,
|
||||||
|
0x44, 0xFF, 0xFD, 0xD1, 0x08, 0x93, 0xC5, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xF6, 0xFF, 0x2B, 0xFF, 0x3C, 0xFF, 0x4C, 0xF6,
|
||||||
|
0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6,
|
||||||
|
0x40, 0xFF, 0x55, 0xF6, 0x40, 0xF6, 0x40, 0xFA, 0x48, 0xA0, 0x08, 0x93, 0xC1, 0x08, 0x93, 0x7A,
|
||||||
|
0xFA, 0x0F, 0xD1, 0x05, 0xC2, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0x65, 0xFF, 0x68, 0xFF, 0x75, 0xFF, 0xAB, 0xFF, 0xC1,
|
||||||
|
0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xFA,
|
||||||
|
0xFF, 0xFA, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xC1, 0x08, 0xC3, 0x75, 0xF6, 0x3F, 0x21, 0x7A,
|
||||||
|
0xFA, 0xC2, 0x05, 0xC2, 0x6F, 0x7A, 0xFF, 0xFD, 0xF4, 0xA0, 0xC2, 0x05, 0xC2, 0x6D, 0x7A, 0xF4,
|
||||||
|
0x97, 0xF4, 0x97, 0xC2, 0x05, 0xC2, 0x6B, 0x72, 0xF4, 0x8E, 0xF4, 0x8E, 0x41, 0x7A, 0xF9, 0xAF,
|
||||||
|
0xA1, 0x05, 0xC2, 0x63, 0xFC, 0xC1, 0x05, 0xC2, 0x77, 0xF4, 0x7C, 0xD6, 0x02, 0x01, 0xC4, 0x61,
|
||||||
|
0x65, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6B, 0x6C, 0x6D, 0x6F, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x75, 0x77, 0x7A, 0xFC, 0x25, 0xFE, 0x31, 0xFE, 0x43, 0xFE, 0x61, 0xFE, 0x77, 0xFD, 0xBB,
|
||||||
|
0xFE, 0xED, 0xFA, 0x06, 0xFA, 0x06, 0xFA, 0x06, 0xFA, 0x06, 0xFD, 0xD3, 0xFA, 0x06, 0xFA, 0x06,
|
||||||
|
0xFF, 0x97, 0xFA, 0x06, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xE8, 0xFF, 0xF5, 0xFA, 0x06, 0xFF, 0xFA,
|
||||||
|
0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF6, 0x93, 0xF6, 0x93, 0xF6, 0x93, 0xF6, 0x93, 0xC2, 0x01, 0xB2,
|
||||||
|
0x63, 0x74, 0xF6, 0xCA, 0xF6, 0xCA, 0xC1, 0x01, 0xB2, 0x72, 0xF4, 0xDA, 0xA0, 0x02, 0x71, 0xC2,
|
||||||
|
0x09, 0x12, 0xC5, 0x64, 0xFC, 0x7F, 0xFF, 0xFD, 0xC2, 0x01, 0x92, 0x70, 0x6B, 0xFC, 0x86, 0xF9,
|
||||||
|
0x33, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x77, 0x7A, 0xFF, 0xEE, 0xFC, 0x53, 0xFC, 0x57, 0xF4, 0x32, 0xFC, 0x64, 0xF4, 0x32, 0xF4,
|
||||||
|
0x32, 0xF4, 0x32, 0xF4, 0x32, 0xF4, 0x32, 0xFF, 0xF7, 0xF4, 0x32, 0xFC, 0x96, 0xFC, 0x96, 0xF4,
|
||||||
|
0x32, 0xF4, 0x32, 0xF4, 0x32, 0xC2, 0x01, 0xB2, 0x6F, 0x72, 0xFF, 0xCC, 0xF6, 0x69, 0xA0, 0x04,
|
||||||
|
0x32, 0x21, 0x7A, 0xFD, 0xC8, 0x01, 0xB2, 0x63, 0x64, 0x6B, 0x72, 0x74, 0xC5, 0x6F, 0x7A, 0xF6,
|
||||||
|
0x63, 0xF6, 0x63, 0xF6, 0x63, 0xF6, 0x63, 0xF6, 0x63, 0xF6, 0x86, 0xFF, 0xFD, 0xF4, 0x7C, 0xC2,
|
||||||
|
0x01, 0xB2, 0x6D, 0x7A, 0xF4, 0x61, 0xF4, 0x61, 0xC2, 0x01, 0xB2, 0x63, 0x6B, 0xF4, 0x54, 0xF4,
|
||||||
|
0x58, 0xC2, 0x01, 0xB2, 0x6D, 0x77, 0xF9, 0x20, 0xF4, 0x4F, 0x53, 0x64, 0x6A, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0xFC, 0xEA,
|
||||||
|
0xFF, 0x11, 0xF5, 0xEC, 0xFF, 0x56, 0xF6, 0x28, 0xF6, 0x0B, 0xFF, 0x63, 0xF6, 0x28, 0xF3, 0xD5,
|
||||||
|
0xFF, 0x6C, 0xF3, 0xD5, 0xF3, 0xD5, 0xFF, 0xBB, 0xFF, 0xCA, 0xFF, 0xE5, 0xFF, 0xEE, 0xF3, 0xD5,
|
||||||
|
0xF3, 0xD5, 0xFF, 0xF7, 0x41, 0x87, 0xFA, 0xF9, 0xA0, 0x09, 0x32, 0xC4, 0x05, 0xC2, 0x63, 0x6B,
|
||||||
|
0x68, 0x7A, 0xFF, 0xFD, 0xFF, 0xFD, 0xF3, 0x46, 0xF3, 0x46, 0xA0, 0x09, 0x52, 0x42, 0xBA, 0xBC,
|
||||||
|
0xFF, 0xFD, 0xF8, 0x5E, 0x41, 0x7A, 0xF7, 0x8A, 0xA1, 0x02, 0x92, 0x72, 0xFC, 0xC1, 0x02, 0x92,
|
||||||
|
0x72, 0xF2, 0x26, 0x41, 0x68, 0xF7, 0x7B, 0xA1, 0x02, 0x92, 0x63, 0xFC, 0x51, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFD, 0x22,
|
||||||
|
0xFD, 0x26, 0xFF, 0xEC, 0xFD, 0x33, 0xFD, 0x43, 0xF4, 0x37, 0xF4, 0x37, 0xF4, 0x37, 0xFF, 0xF1,
|
||||||
|
0xF4, 0x37, 0xFD, 0x52, 0xFF, 0xFB, 0xFD, 0x4C, 0xFD, 0x4C, 0xFD, 0x52, 0xF4, 0x37, 0xF4, 0x37,
|
||||||
|
0xC3, 0x05, 0xC2, 0xC5, 0x6F, 0x7A, 0xFF, 0xAD, 0xFF, 0xCC, 0xF2, 0xE1, 0xC2, 0x05, 0xC2, 0x63,
|
||||||
|
0x6B, 0xFF, 0x8C, 0xFF, 0x8C, 0xA0, 0x09, 0x92, 0xC1, 0x03, 0x12, 0x75, 0xF4, 0x5F, 0xA0, 0x09,
|
||||||
|
0xE1, 0x44, 0xBA, 0x82, 0x9B, 0xBC, 0xFF, 0xF4, 0xFF, 0xF7, 0xF4, 0x42, 0xFF, 0xFD, 0x41, 0x68,
|
||||||
|
0xF4, 0x49, 0x21, 0x63, 0xFC, 0xA1, 0x02, 0xB2, 0x75, 0xFD, 0x41, 0x7A, 0xF4, 0x3D, 0x21, 0x63,
|
||||||
|
0xFC, 0x21, 0x99, 0xFD, 0x41, 0xBC, 0xF4, 0x33, 0x42, 0xC5, 0x70, 0xFF, 0xFC, 0xF4, 0x2F, 0x42,
|
||||||
|
0x63, 0x74, 0xFF, 0xEB, 0xF4, 0x28, 0x21, 0x70, 0xF9, 0xA3, 0x02, 0xB2, 0xC4, 0x61, 0x65, 0xE8,
|
||||||
|
0xEF, 0xFD, 0x41, 0x6A, 0xF4, 0x15, 0xA2, 0x02, 0xB2, 0x61, 0x6F, 0xFC, 0xFC, 0xA0, 0x09, 0xB3,
|
||||||
|
0x21, 0x63, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x7A, 0xF4, 0x21, 0x72, 0xFD, 0x21,
|
||||||
|
0x74, 0xFD, 0xA2, 0x02, 0x71, 0x63, 0x73, 0xF4, 0xFD, 0x41, 0x87, 0xF3, 0xDA, 0xC2, 0x02, 0xB2,
|
||||||
|
0x68, 0x7A, 0xF6, 0xB1, 0xF6, 0xB1, 0x42, 0xBA, 0xBC, 0xF8, 0x59, 0xF8, 0x59, 0xA1, 0x02, 0xB2,
|
||||||
|
0xC5, 0xF9, 0xC2, 0x02, 0xB2, 0x6D, 0x7A, 0xF6, 0x9C, 0xF6, 0x9C, 0xD5, 0x09, 0x72, 0xC5, 0x6D,
|
||||||
|
0x72, 0x77, 0x6F, 0x75, 0x7A, 0xC4, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6E,
|
||||||
|
0x70, 0x73, 0x74, 0xFF, 0x76, 0xFF, 0x8A, 0xFF, 0xAE, 0xFF, 0xBB, 0xFF, 0xD7, 0xFE, 0x21, 0xF3,
|
||||||
|
0x52, 0xFF, 0xDE, 0xF3, 0x72, 0xFF, 0xE2, 0xFF, 0xF2, 0xF3, 0x72, 0xF3, 0x72, 0xF3, 0x72, 0xF3,
|
||||||
|
0x72, 0xF3, 0x72, 0xF3, 0x72, 0xF3, 0x72, 0xF3, 0x72, 0xFF, 0xF7, 0xF3, 0x72, 0xC2, 0x02, 0x92,
|
||||||
|
0x6D, 0x7A, 0xF0, 0xF6, 0xF0, 0xF6, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B,
|
||||||
|
0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xB5, 0xFB, 0xF8, 0xFB, 0xFC, 0xF3, 0x0D,
|
||||||
|
0xFC, 0x09, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D,
|
||||||
|
0xFC, 0x22, 0xFF, 0xF7, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xA0, 0x02, 0xF1, 0xA1, 0x02, 0xD2,
|
||||||
|
0x6B, 0xFD, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xFF, 0xFB, 0xF2, 0xF7, 0xF2, 0xF7, 0xF2, 0xF7, 0xA0,
|
||||||
|
0x0A, 0x12, 0xA0, 0x0A, 0xC2, 0x21, 0x63, 0xFD, 0xA1, 0x0A, 0x32, 0x79, 0xFD, 0x21, 0xBC, 0xFB,
|
||||||
|
0x21, 0xC5, 0xFD, 0xA1, 0x0B, 0x42, 0x75, 0xFD, 0xA0, 0x0A, 0xE3, 0xA0, 0x0B, 0x42, 0x24, 0xBA,
|
||||||
|
0x82, 0xBC, 0x9B, 0xE1, 0xF5, 0xFA, 0xFD, 0xA0, 0x0A, 0x32, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD,
|
||||||
|
0xA1, 0x0B, 0x62, 0x75, 0xFD, 0x21, 0x74, 0xF2, 0x21, 0x61, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6B,
|
||||||
|
0xE9, 0x21, 0x6C, 0xE6, 0xA0, 0x0A, 0x53, 0xA4, 0x0B, 0x62, 0x61, 0x75, 0x79, 0x7A, 0xF4, 0xF7,
|
||||||
|
0xFA, 0xFD, 0xA1, 0x0B, 0x62, 0x6D, 0xD5, 0xA0, 0x02, 0xD1, 0xA0, 0x0B, 0x13, 0x41, 0x6D, 0xFF,
|
||||||
|
0xA5, 0x41, 0x6A, 0xFF, 0xA1, 0x22, 0x72, 0x6F, 0xB3, 0xFC, 0xC6, 0x02, 0xD2, 0x61, 0x67, 0x69,
|
||||||
|
0x6A, 0x6C, 0x77, 0xFF, 0xF0, 0xFF, 0xAE, 0xFF, 0xF3, 0xFF, 0x98, 0xFF, 0x98, 0xFF, 0xFB, 0x41,
|
||||||
|
0x87, 0xFF, 0x9C, 0xC2, 0x0B, 0x62, 0x68, 0x7A, 0xFF, 0x67, 0xFF, 0x67, 0xA0, 0x0B, 0x81, 0x22,
|
||||||
|
0xBA, 0xBC, 0xFD, 0xFD, 0xC2, 0x0B, 0x62, 0xC5, 0x7A, 0xFF, 0xFB, 0xFF, 0x56, 0xA0, 0x0B, 0x62,
|
||||||
|
0xC1, 0x0B, 0x62, 0x7A, 0xFF, 0x7B, 0xD5, 0x09, 0xF2, 0xC5, 0x6D, 0x72, 0x75, 0x79, 0x7A, 0xC4,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6E, 0x70, 0x73, 0x74, 0x77, 0xFF, 0x78,
|
||||||
|
0xFF, 0x8A, 0xFF, 0xA1, 0xFF, 0xAC, 0xFF, 0xB1, 0xFF, 0xC4, 0xFF, 0xD9, 0xFF, 0xDD, 0xFF, 0xEE,
|
||||||
|
0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7,
|
||||||
|
0xFF, 0xF7, 0xFF, 0xFA, 0xFF, 0xF7, 0xFF, 0xF7, 0x41, 0x87, 0xF2, 0x01, 0x41, 0x7A, 0xFF, 0x13,
|
||||||
|
0xA1, 0x06, 0xC2, 0x72, 0xFC, 0xC2, 0x06, 0xC2, 0x68, 0x7A, 0xF6, 0xBA, 0xF6, 0xBA, 0xA0, 0x0A,
|
||||||
|
0x84, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD, 0x41, 0x7A, 0xFE, 0xE3, 0xA1, 0x06, 0xC2, 0x72, 0xFC,
|
||||||
|
0xC2, 0x06, 0xC2, 0x6C, 0x72, 0xF6, 0x9F, 0xF6, 0x9F, 0x41, 0x68, 0xFE, 0xD1, 0xA1, 0x06, 0xC2,
|
||||||
|
0x63, 0xFC, 0x41, 0xBC, 0xFE, 0xC8, 0xC2, 0x06, 0xC2, 0xC5, 0x7A, 0xFF, 0xFC, 0xF6, 0x89, 0xC3,
|
||||||
|
0x06, 0xC2, 0x63, 0x6D, 0x7A, 0xFF, 0xEA, 0xF6, 0x80, 0xF6, 0x80, 0xC2, 0x06, 0xC2, 0x6B, 0x72,
|
||||||
|
0xF6, 0x74, 0xF6, 0x74, 0x53, 0xC5, 0x64, 0xC4, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFE, 0xAE, 0xFF, 0x62, 0xFF, 0xA4, 0xFF, 0xAC,
|
||||||
|
0xFF, 0xB1, 0xFF, 0xC0, 0xF6, 0xE3, 0xFF, 0xC7, 0xF6, 0xE3, 0xFF, 0xCC, 0xF6, 0xE3, 0xF6, 0xE3,
|
||||||
|
0xF6, 0xE3, 0xFF, 0xD9, 0xFF, 0xE2, 0xFF, 0xEB, 0xFF, 0xF7, 0xF6, 0xE3, 0xF6, 0xE3, 0x21, 0x65,
|
||||||
|
0xC6, 0x21, 0x7A, 0xFD, 0xC2, 0x05, 0xC2, 0x6F, 0x72, 0xFE, 0x32, 0xFF, 0xFD, 0xC1, 0x03, 0x52,
|
||||||
|
0x72, 0xF6, 0x9A, 0x41, 0x9B, 0xF4, 0x6B, 0x41, 0x7A, 0xF6, 0x18, 0x21, 0x72, 0xFC, 0x41, 0x72,
|
||||||
|
0xF4, 0x60, 0xC6, 0x03, 0x32, 0x65, 0x75, 0xC5, 0x62, 0x6D, 0x74, 0xFF, 0xEB, 0xF1, 0x95, 0xFF,
|
||||||
|
0xF1, 0xFF, 0xF9, 0xEF, 0x01, 0xFF, 0xFC, 0x21, 0x7A, 0xEB, 0xC2, 0x05, 0xC2, 0x6F, 0x7A, 0xFF,
|
||||||
|
0xFD, 0xEF, 0xE7, 0x41, 0x65, 0xF1, 0x74, 0xA1, 0x03, 0x32, 0x69, 0xFC, 0x21, 0x62, 0xFB, 0xC2,
|
||||||
|
0x05, 0xC2, 0x75, 0x7A, 0xFF, 0xFD, 0xEF, 0xD2, 0xC2, 0x05, 0xC2, 0x63, 0x77, 0xFB, 0x44, 0xEF,
|
||||||
|
0xC9, 0xC2, 0x02, 0x92, 0x6B, 0x72, 0xEE, 0xC2, 0xEE, 0xC2, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64,
|
||||||
|
0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xF9, 0xC4, 0xF9, 0xC8,
|
||||||
|
0xFC, 0x8E, 0xF9, 0xD5, 0xF9, 0xE5, 0xF0, 0xD9, 0xF0, 0xD9, 0xF0, 0xD9, 0xF0, 0xD9, 0xF0, 0xD9,
|
||||||
|
0xF0, 0xD9, 0xF0, 0xD9, 0xF9, 0xEE, 0xF9, 0xEE, 0xFF, 0xF7, 0xF0, 0xD9, 0xF0, 0xD9, 0xA1, 0x05,
|
||||||
|
0xC2, 0x79, 0xCC, 0x41, 0x87, 0xF4, 0xA8, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF4, 0xA4, 0xF4, 0xA4,
|
||||||
|
0xF4, 0xA4, 0xF4, 0xA4, 0x43, 0x9B, 0xBA, 0xBC, 0xF0, 0x89, 0xF0, 0x89, 0xF0, 0x89, 0xCD, 0x00,
|
||||||
|
0x91, 0xC4, 0xC5, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x70, 0x73, 0x77, 0xF9, 0x70,
|
||||||
|
0xFF, 0xF6, 0xF9, 0x81, 0xF9, 0x91, 0xF0, 0x85, 0xF0, 0x85, 0xF0, 0x85, 0xF0, 0x85, 0xF0, 0x85,
|
||||||
|
0xF0, 0x85, 0xF0, 0x85, 0xF9, 0x9A, 0xF0, 0x85, 0xC2, 0x00, 0x91, 0x68, 0x7A, 0xF4, 0x63, 0xF4,
|
||||||
|
0x63, 0x44, 0xBA, 0x82, 0x9B, 0xBC, 0xEE, 0x32, 0xF0, 0x4C, 0xF0, 0x4C, 0xFA, 0x05, 0xC1, 0x00,
|
||||||
|
0x71, 0x72, 0xFB, 0x0E, 0xD0, 0x00, 0x91, 0xC5, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||||
|
0x6C, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xED, 0xF9, 0x2A, 0xF9, 0x3B, 0xF9, 0x4B, 0xF0,
|
||||||
|
0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF9,
|
||||||
|
0x54, 0xF0, 0x3F, 0xFF, 0xFA, 0xF4, 0x47, 0xC1, 0x00, 0x91, 0x7A, 0xF4, 0x14, 0xD1, 0x02, 0x41,
|
||||||
|
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77,
|
||||||
|
0x7A, 0xFF, 0x66, 0xFF, 0x6A, 0xFF, 0x81, 0xFF, 0xAB, 0xFF, 0xC7, 0xEE, 0xE4, 0xEE, 0xE4, 0xEE,
|
||||||
|
0xE4, 0xEE, 0xE4, 0xEE, 0xE4, 0xEE, 0xE4, 0xEE, 0xE4, 0xFF, 0xFA, 0xFF, 0xFA, 0xEE, 0xE4, 0xEE,
|
||||||
|
0xE4, 0xEE, 0xE4, 0x53, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70,
|
||||||
|
0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0x6F, 0xFB, 0x61, 0xF8, 0x99, 0xF4, 0x3E, 0xFB, 0x68, 0xFB,
|
||||||
|
0xCD, 0xF4, 0x3E, 0xF4, 0x3E, 0xF4, 0x3E, 0xF4, 0x3E, 0xF4, 0x3E, 0xFB, 0xD9, 0xFE, 0x91, 0xFE,
|
||||||
|
0xC7, 0xFE, 0xDC, 0xFA, 0x20, 0xFE, 0xE5, 0xFF, 0x2B, 0xFA, 0x32, 0xFF, 0xCA, 0x21, 0x65, 0xC6,
|
||||||
|
0xD8, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B,
|
||||||
|
0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xEF, 0x09, 0xEF, 0x0D, 0xFA,
|
||||||
|
0xEA, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xF3, 0x40, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xFF,
|
||||||
|
0xFD, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF,
|
||||||
|
0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xA0, 0x06, 0x11, 0x21, 0x87,
|
||||||
|
0xFD, 0x41, 0x6F, 0xEF, 0xA8, 0x21, 0x69, 0xFC, 0xA1, 0x06, 0x11, 0x6D, 0xFD, 0x23, 0x9B, 0xBA,
|
||||||
|
0xBC, 0xFB, 0xEE, 0xEE, 0x42, 0x85, 0x99, 0xF3, 0x94, 0xF3, 0x94, 0xA0, 0x05, 0x92, 0xA1, 0x05,
|
||||||
|
0x72, 0x7A, 0xFD, 0x21, 0x63, 0xFB, 0xA2, 0x03, 0xA2, 0xC4, 0x6F, 0xEE, 0xFD, 0x44, 0x82, 0x9B,
|
||||||
|
0xBA, 0xBC, 0xFF, 0xF9, 0xF0, 0x56, 0xF0, 0x56, 0xF0, 0x56, 0x41, 0x67, 0xF3, 0x6E, 0x21, 0x7A,
|
||||||
|
0xFC, 0xA1, 0x01, 0xB2, 0x75, 0xFD, 0x41, 0x87, 0xF3, 0x62, 0x41, 0x6F, 0xF3, 0x5E, 0x22, 0xC4,
|
||||||
|
0x73, 0xF8, 0xFC, 0x41, 0x84, 0xF3, 0x55, 0x42, 0xC5, 0x6E, 0xFF, 0xFC, 0xF3, 0x51, 0x41, 0xBA,
|
||||||
|
0xF3, 0x4A, 0x42, 0xC5, 0x7A, 0xFF, 0xFC, 0xF3, 0x46, 0x21, 0x64, 0xB2, 0x22, 0x85, 0x99, 0xAF,
|
||||||
|
0xFD, 0x21, 0x7A, 0xAA, 0x22, 0x63, 0x74, 0xA7, 0xA7, 0x41, 0x6E, 0xFF, 0xA2, 0xA4, 0x0B, 0x93,
|
||||||
|
0xC4, 0x65, 0x75, 0x79, 0xEF, 0xF4, 0xF7, 0xFC, 0xA4, 0x01, 0x61, 0x61, 0x6F, 0x79, 0x7A, 0xC6,
|
||||||
|
0xCF, 0xDA, 0xF5, 0xC3, 0x05, 0xC2, 0x6E, 0x68, 0x7A, 0xFA, 0x45, 0xED, 0x8E, 0xED, 0x8E, 0xC2,
|
||||||
|
0x05, 0xC2, 0xC5, 0x7A, 0xF7, 0x88, 0xED, 0x82, 0xA0, 0x0C, 0x52, 0x41, 0xBC, 0xF2, 0xA0, 0xC4,
|
||||||
|
0x05, 0xC2, 0x74, 0xC5, 0x6D, 0x7A, 0xFF, 0xF9, 0xFF, 0xFC, 0xF4, 0xBA, 0xED, 0x72, 0x41, 0x68,
|
||||||
|
0xF2, 0x8D, 0xC2, 0x05, 0xC2, 0x63, 0x7A, 0xFF, 0xFC, 0xED, 0x5F, 0xC1, 0x05, 0xC2, 0x6B, 0xED,
|
||||||
|
0x56, 0xC1, 0x05, 0xC2, 0x77, 0xFA, 0x07, 0xD1, 0x00, 0x81, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||||
|
0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xF9, 0xFD, 0xF7, 0x35, 0xF2,
|
||||||
|
0xDA, 0xFF, 0xBC, 0xFF, 0xC8, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2,
|
||||||
|
0xDA, 0xF2, 0xDA, 0xFF, 0xD8, 0xFF, 0xEB, 0xFF, 0xF4, 0xF2, 0xDA, 0xFF, 0xFA, 0xC1, 0x00, 0x81,
|
||||||
|
0x62, 0xF2, 0xA4, 0x41, 0x7A, 0xED, 0x5C, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD,
|
||||||
|
0xC1, 0x01, 0xB2, 0x7A, 0xED, 0xC0, 0xA0, 0x0C, 0x23, 0x21, 0x6D, 0xFD, 0xD5, 0x00, 0xF2, 0xC5,
|
||||||
|
0x6C, 0x72, 0x65, 0x69, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x6F, 0x70,
|
||||||
|
0x73, 0x74, 0x75, 0x77, 0xFF, 0x01, 0xFF, 0x15, 0xFF, 0x5C, 0xFF, 0xAB, 0xFF, 0xE1, 0xEF, 0x5A,
|
||||||
|
0xEF, 0x79, 0xEF, 0x89, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43,
|
||||||
|
0xED, 0x43, 0xFF, 0xF1, 0xED, 0x43, 0xFF, 0xF4, 0xED, 0x43, 0xFF, 0xFD, 0xED, 0x43, 0x41, 0x6F,
|
||||||
|
0xED, 0x01, 0xC2, 0x01, 0xB2, 0x75, 0x7A, 0xF2, 0x36, 0xFF, 0xFC, 0x41, 0x74, 0xF2, 0x2D, 0x21,
|
||||||
|
0x99, 0xFC, 0x41, 0x70, 0xF2, 0x26, 0x41, 0x6E, 0xF2, 0x22, 0x21, 0x69, 0xFC, 0x21, 0x62, 0xFD,
|
||||||
|
0x41, 0x69, 0xF2, 0x18, 0x41, 0xC4, 0xFE, 0xB2, 0xC2, 0x02, 0x41, 0x65, 0x77, 0xFF, 0xFC, 0xF2,
|
||||||
|
0x10, 0xA6, 0x01, 0xB2, 0xC4, 0x61, 0x6F, 0x75, 0x77, 0x7A, 0xDE, 0xE1, 0xEC, 0xDA, 0xEF, 0xF7,
|
||||||
|
0xA0, 0x0C, 0x02, 0xA1, 0x0B, 0xC2, 0x72, 0xFD, 0xA1, 0x0B, 0xC2, 0x6D, 0xF8, 0x22, 0x61, 0x65,
|
||||||
|
0xF6, 0xFB, 0xA1, 0x0C, 0x92, 0x69, 0xFB, 0xC1, 0x05, 0xC2, 0x6B, 0xF4, 0x06, 0xC1, 0x05, 0xC2,
|
||||||
|
0x63, 0xFE, 0xF1, 0xC2, 0x05, 0xC2, 0xC5, 0x7A, 0xFE, 0xD8, 0xEC, 0x4E, 0xC2, 0x05, 0xC2, 0x63,
|
||||||
|
0x6B, 0xFE, 0xE2, 0xEC, 0x45, 0xD1, 0x00, 0x81, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xF8, 0xEF, 0xF6, 0x27, 0xF1, 0xCC, 0xF5,
|
||||||
|
0x81, 0xFE, 0xBA, 0xF1, 0xCC, 0xF1, 0xCC, 0xF1, 0xCC, 0xF1, 0xCC, 0xF1, 0xCC, 0xFF, 0xE2, 0xFF,
|
||||||
|
0xE8, 0xFF, 0xEE, 0xF5, 0x93, 0xFF, 0xF7, 0xF1, 0xCC, 0xF1, 0xCC, 0xA0, 0x0C, 0x72, 0x43, 0xBA,
|
||||||
|
0x9B, 0xBC, 0xEC, 0xC2, 0xEE, 0x65, 0xFF, 0xFD, 0x41, 0x75, 0xEF, 0x55, 0xC1, 0x01, 0xB2, 0x65,
|
||||||
|
0xF8, 0x35, 0x41, 0x73, 0xEC, 0x3D, 0x42, 0x63, 0x6D, 0xFE, 0xDD, 0xFE, 0xF0, 0xD8, 0x00, 0xF2,
|
||||||
|
0x69, 0x72, 0x7A, 0x65, 0xC5, 0xC4, 0x61, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x6E, 0x6F, 0x70, 0x73, 0x74, 0x75, 0x77, 0xFF, 0x35, 0xFF, 0x64, 0xFF, 0x85, 0xFF, 0xA8,
|
||||||
|
0xFF, 0xE1, 0xEE, 0x49, 0xFF, 0xEB, 0xFF, 0xEF, 0xEE, 0x68, 0xEE, 0x78, 0xEC, 0x32, 0xEC, 0x32,
|
||||||
|
0xEC, 0x32, 0xEC, 0x32, 0xEE, 0x85, 0xEC, 0x32, 0xEC, 0x32, 0xEC, 0x32, 0xFF, 0xF5, 0xEC, 0x32,
|
||||||
|
0xFE, 0xE3, 0xEC, 0x32, 0xFF, 0xF9, 0xEC, 0x32, 0x41, 0x82, 0xF1, 0x48, 0x21, 0xC5, 0xFC, 0x21,
|
||||||
|
0x68, 0xFD, 0xA1, 0x02, 0x01, 0x63, 0xFD, 0xA0, 0x0B, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x9B, 0xFD,
|
||||||
|
0xA0, 0x0C, 0xB2, 0x21, 0x6F, 0xFD, 0x22, 0xC5, 0x65, 0xF7, 0xFD, 0x41, 0x7A, 0xEE, 0xD2, 0xC1,
|
||||||
|
0x06, 0x11, 0x72, 0xEE, 0x0F, 0xC2, 0x02, 0x01, 0x68, 0x7A, 0xFF, 0xFA, 0xFD, 0x46, 0xA0, 0x02,
|
||||||
|
0x01, 0x41, 0x6B, 0xF1, 0xE6, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0xB3,
|
||||||
|
0xFD, 0x41, 0x72, 0xEC, 0x6C, 0x21, 0x74, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0xA2, 0x02,
|
||||||
|
0x01, 0xC3, 0x6E, 0xF0, 0xFD, 0x41, 0x6D, 0xF0, 0xCC, 0xC2, 0x02, 0x01, 0x61, 0x72, 0xFF, 0xFC,
|
||||||
|
0xFD, 0x12, 0x41, 0x68, 0xEB, 0xEE, 0xA1, 0x02, 0x01, 0x63, 0xFC, 0xA0, 0x0C, 0xD2, 0x21, 0xBC,
|
||||||
|
0xFD, 0x21, 0x99, 0xBD, 0x21, 0xC4, 0xFD, 0xA0, 0x07, 0x02, 0x23, 0xC5, 0x74, 0x7A, 0xF4, 0xFA,
|
||||||
|
0xFD, 0xA0, 0x05, 0xE2, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x04, 0x32, 0x73, 0xFD, 0x21,
|
||||||
|
0x6D, 0xFB, 0x21, 0x65, 0xFD, 0xC2, 0x02, 0x01, 0x69, 0x7A, 0xFF, 0xFD, 0xFC, 0xD6, 0x52, 0xC4,
|
||||||
|
0xC5, 0x62, 0x64, 0x74, 0x6C, 0x61, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6D, 0x70, 0x72, 0x73, 0x77,
|
||||||
|
0x7A, 0xFC, 0xD0, 0xFC, 0xDF, 0xFD, 0xFE, 0xFF, 0x0F, 0xFF, 0x64, 0xFF, 0x78, 0xFF, 0x7D, 0xFF,
|
||||||
|
0x87, 0xFF, 0x90, 0xFF, 0xB0, 0xFF, 0x90, 0xFF, 0xBB, 0xFF, 0x90, 0xFF, 0xC8, 0xFF, 0xDC, 0xFF,
|
||||||
|
0xF7, 0xFF, 0x90, 0xFF, 0x90, 0x41, 0x77, 0xF1, 0x42, 0x21, 0x6F, 0xFC, 0x21, 0x6B, 0xFD, 0x21,
|
||||||
|
0x73, 0xFD, 0xA1, 0x00, 0x61, 0x61, 0xFD, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0xFB, 0xE9,
|
||||||
|
0x23, 0xE9, 0x23, 0xE9, 0x23, 0xE9, 0x23, 0xC1, 0x00, 0x71, 0x72, 0xE9, 0x9C, 0x41, 0x72, 0xF9,
|
||||||
|
0x4A, 0x41, 0x64, 0xE9, 0x92, 0xA2, 0x00, 0x71, 0x62, 0x6D, 0xF8, 0xFC, 0x41, 0x6B, 0xE9, 0x9A,
|
||||||
|
0x21, 0x6D, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x42, 0x85, 0x99, 0xF0,
|
||||||
|
0xCF, 0xF0, 0xCF, 0x41, 0x7A, 0xF0, 0xC8, 0x21, 0x63, 0xFC, 0xC7, 0x06, 0xE2, 0x6B, 0x6D, 0x6F,
|
||||||
|
0x70, 0xC4, 0x65, 0x79, 0xFF, 0xCD, 0xEF, 0x91, 0xFF, 0xDB, 0xFF, 0xEF, 0xFF, 0xF2, 0xFF, 0xFD,
|
||||||
|
0xF0, 0xC1, 0x21, 0x82, 0xE8, 0x21, 0xC5, 0xFD, 0x21, 0xB3, 0xFD, 0xC1, 0x00, 0x51, 0x68, 0xE8,
|
||||||
|
0xBF, 0x41, 0x6B, 0xEC, 0x49, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x82, 0xFD, 0xA0, 0x0C,
|
||||||
|
0xF3, 0xA1, 0x02, 0x22, 0x6E, 0xFD, 0xC5, 0x02, 0x02, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xEA, 0xDA,
|
||||||
|
0xFF, 0xFB, 0xEA, 0xDA, 0xEA, 0xDA, 0xEA, 0xDA, 0x41, 0x6B, 0xEA, 0x67, 0xA0, 0x0D, 0x22, 0x21,
|
||||||
|
0x6C, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x42, 0x72, 0x73, 0xEA, 0x09, 0xEA, 0x09, 0x21,
|
||||||
|
0x65, 0xF9, 0x21, 0x65, 0xFD, 0xA0, 0x0D, 0x43, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x65,
|
||||||
|
0xFD, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x26, 0xC5, 0x72, 0x63, 0x65, 0x70,
|
||||||
|
0x7A, 0xB1, 0xBC, 0xCE, 0xDB, 0xE8, 0xFD, 0x41, 0x74, 0xEA, 0x2E, 0x21, 0x65, 0xFC, 0xA1, 0x0D,
|
||||||
|
0x72, 0x73, 0xFD, 0x21, 0x87, 0xFB, 0x41, 0x9B, 0xEB, 0x13, 0x21, 0xC5, 0xFC, 0x21, 0x6F, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0x22, 0xC4, 0x63, 0xF0, 0xFD, 0x21, 0x99, 0xFB, 0x41, 0x77, 0xEA, 0xF8, 0x21,
|
||||||
|
0x72, 0xFC, 0x41, 0x6E, 0xEC, 0xDA, 0x23, 0xC4, 0x65, 0x6F, 0xF2, 0xF9, 0xFC, 0xC1, 0x03, 0xA2,
|
||||||
|
0x6B, 0xE9, 0xA4, 0x41, 0x63, 0xEC, 0x06, 0x45, 0x82, 0x9B, 0xBA, 0xBC, 0x84, 0xFF, 0xF6, 0xEB,
|
||||||
|
0xFC, 0xEB, 0xFC, 0xEB, 0xFC, 0xFF, 0xFC, 0xC1, 0x07, 0xE2, 0x75, 0xEA, 0x39, 0xA0, 0x0E, 0x83,
|
||||||
|
0x21, 0x64, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0xA1, 0x07, 0xE2, 0x72, 0xFD, 0x44, 0xBA,
|
||||||
|
0x82, 0x9B, 0xBC, 0xF1, 0x2C, 0xFF, 0xE9, 0xFF, 0xFB, 0xF1, 0x52, 0x41, 0x77, 0xEA, 0x15, 0xA1,
|
||||||
|
0x05, 0xC2, 0x61, 0xFC, 0xC1, 0x02, 0x22, 0x6E, 0xF1, 0x71, 0x21, 0x7A, 0xFA, 0x22, 0x63, 0x74,
|
||||||
|
0xFD, 0xEE, 0x21, 0x99, 0xFB, 0x41, 0xBC, 0xE9, 0xFB, 0x21, 0xC5, 0xFC, 0x21, 0xB3, 0xFD, 0x41,
|
||||||
|
0x69, 0xF1, 0x49, 0x42, 0xC5, 0x70, 0xFF, 0xF2, 0xE9, 0xED, 0x41, 0xB3, 0xE9, 0xE6, 0x44, 0xC3,
|
||||||
|
0x61, 0x6F, 0x79, 0xFF, 0xFC, 0xE9, 0xE2, 0xE9, 0xE2, 0xE9, 0xE2, 0xA0, 0x03, 0x32, 0x44, 0xC5,
|
||||||
|
0x62, 0x63, 0x7A, 0xFF, 0xD7, 0xFF, 0xF0, 0xF1, 0x20, 0xFF, 0xFD, 0x41, 0x67, 0xE9, 0xC5, 0x21,
|
||||||
|
0x7A, 0xFC, 0x41, 0x65, 0xE9, 0xBE, 0xC9, 0x05, 0xC2, 0xC4, 0xC3, 0x77, 0x61, 0x65, 0x6F, 0x75,
|
||||||
|
0x79, 0x7A, 0xFF, 0xBC, 0xFF, 0xC6, 0xFF, 0xC9, 0xFF, 0xCD, 0xF1, 0x1C, 0xFF, 0xE8, 0xFF, 0xF9,
|
||||||
|
0xFF, 0xF5, 0xFF, 0xFC, 0x42, 0x6A, 0x72, 0xE9, 0x9F, 0xE9, 0x9C, 0x21, 0xB3, 0xF9, 0x41, 0x6A,
|
||||||
|
0xE9, 0x92, 0x43, 0x69, 0x6A, 0x72, 0xE9, 0x8E, 0xE9, 0x8E, 0xF0, 0xDC, 0xA3, 0x05, 0xC2, 0xC3,
|
||||||
|
0x61, 0x6F, 0xEF, 0xF2, 0xF6, 0x41, 0x70, 0xF2, 0x81, 0xA1, 0x01, 0x61, 0x6C, 0xFC, 0xA0, 0x0D,
|
||||||
|
0xD5, 0xA1, 0x01, 0x92, 0x73, 0xFD, 0x41, 0x68, 0xE7, 0xBD, 0xA1, 0x01, 0x92, 0x63, 0xFC, 0xC2,
|
||||||
|
0x01, 0x92, 0x63, 0x7A, 0xFF, 0xF7, 0xED, 0xDC, 0xC1, 0x01, 0x92, 0x6B, 0xE8, 0xD0, 0xD2, 0x01,
|
||||||
|
0x61, 0x6A, 0x6B, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x77, 0x7A, 0xEB, 0xFF, 0xFF, 0xE3, 0xF0, 0xF6, 0xF0, 0xFA, 0xE8, 0xD5, 0xF1, 0x07, 0xF1,
|
||||||
|
0x17, 0xE8, 0xD5, 0xE8, 0xD5, 0xE8, 0xD5, 0xE8, 0xD5, 0xE8, 0xD5, 0xFF, 0xEC, 0xF1, 0x30, 0xFF,
|
||||||
|
0xF1, 0xFF, 0xFA, 0xE8, 0xD5, 0xE8, 0xD5, 0x42, 0x75, 0x6E, 0xEB, 0xC6, 0xED, 0xFA, 0x41, 0xB3,
|
||||||
|
0xF1, 0x3E, 0x41, 0x64, 0xF1, 0x37, 0x21, 0x61, 0xFC, 0x41, 0x6A, 0xF1, 0x36, 0x41, 0x61, 0xF1,
|
||||||
|
0x2F, 0x45, 0xC3, 0x69, 0x6F, 0x72, 0x73, 0xFF, 0xED, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFC, 0xF1,
|
||||||
|
0x52, 0x41, 0x63, 0xF1, 0x18, 0x21, 0x6F, 0xFC, 0x21, 0x68, 0xFD, 0x42, 0x6D, 0x64, 0xF1, 0x0E,
|
||||||
|
0xEA, 0xDC, 0x41, 0x67, 0xF1, 0x07, 0x21, 0x99, 0xFC, 0x41, 0x73, 0xF1, 0x00, 0x22, 0xC4, 0x65,
|
||||||
|
0xF9, 0xFC, 0x43, 0x69, 0x6E, 0x72, 0xF1, 0x1B, 0xF0, 0xF7, 0xFF, 0xFB, 0x41, 0x65, 0xF0, 0xF3,
|
||||||
|
0x21, 0x69, 0xFC, 0x41, 0x77, 0xF0, 0xE6, 0x21, 0x79, 0xFC, 0x41, 0x6B, 0xF0, 0xDF, 0x21, 0x61,
|
||||||
|
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x69, 0x74, 0xFF, 0xFD, 0xF0, 0xF0, 0xCA, 0x01,
|
||||||
|
0x61, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x70, 0x72, 0x73, 0x6C, 0xFF, 0xA3, 0xFF, 0xBA, 0xFF,
|
||||||
|
0xBD, 0xF0, 0xCB, 0xF0, 0xCB, 0xFF, 0xD4, 0xFF, 0xE2, 0xFF, 0xE9, 0xFF, 0xF9, 0xF0, 0xFC, 0x41,
|
||||||
|
0x61, 0xF0, 0xAA, 0x41, 0x6C, 0xF0, 0xA6, 0x21, 0x61, 0xFC, 0xA0, 0x04, 0x02, 0x21, 0x6E, 0xFD,
|
||||||
|
0xA1, 0x07, 0x73, 0x79, 0xFD, 0x21, 0x7A, 0xFB, 0x21, 0x63, 0xFD, 0x22, 0x74, 0x7A, 0xE4, 0xFD,
|
||||||
|
0xC5, 0x01, 0x61, 0x63, 0x64, 0x70, 0x72, 0x73, 0xF0, 0x8C, 0xF0, 0x8C, 0xFF, 0xDF, 0xFF, 0xE7,
|
||||||
|
0xFF, 0xFB, 0x41, 0x72, 0xEF, 0xB3, 0x41, 0x74, 0xEF, 0xAF, 0x43, 0x6B, 0x6D, 0x73, 0xFF, 0xF8,
|
||||||
|
0xEF, 0xAB, 0xFF, 0xFC, 0x41, 0x69, 0xEF, 0xA1, 0x43, 0x85, 0x87, 0x99, 0xEF, 0x9D, 0xEF, 0x9D,
|
||||||
|
0xEF, 0x9D, 0x41, 0x82, 0xEF, 0x93, 0x41, 0x6E, 0xEF, 0x8F, 0x48, 0xC4, 0xC5, 0x63, 0x65, 0x6C,
|
||||||
|
0x6D, 0x6F, 0x73, 0xFF, 0xEE, 0xFF, 0xF8, 0xEF, 0x8B, 0xFF, 0xFC, 0xEF, 0x8B, 0xEF, 0x8B, 0xEF,
|
||||||
|
0x8B, 0xEF, 0x8B, 0x21, 0x69, 0xE7, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42, 0x6C, 0x6D, 0xFF,
|
||||||
|
0xFD, 0xEF, 0x69, 0x42, 0x65, 0x6F, 0xFF, 0xF9, 0xF0, 0x65, 0xA0, 0x0E, 0x23, 0x21, 0x72, 0xFD,
|
||||||
|
0xC5, 0x03, 0xA2, 0x61, 0x62, 0x65, 0x69, 0x77, 0xFF, 0xAA, 0xFF, 0xB4, 0xEF, 0x55, 0xFF, 0xF3,
|
||||||
|
0xFF, 0xFD, 0x41, 0x77, 0xE9, 0xD5, 0x21, 0xB3, 0xFC, 0xC1, 0x05, 0xC2, 0xC5, 0xF1, 0x2E, 0xDA,
|
||||||
|
0x07, 0x02, 0xC4, 0xC5, 0x6D, 0x6E, 0x72, 0x77, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x7A, 0xC3,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x70, 0x73, 0x74, 0xEE, 0xD1, 0xFD, 0xAF,
|
||||||
|
0xEF, 0x1A, 0xFD, 0xC0, 0xFE, 0x17, 0xFE, 0x4D, 0xFE, 0x5A, 0xFE, 0x7F, 0xFE, 0xB8, 0xFF, 0x2F,
|
||||||
|
0xFF, 0x71, 0xEA, 0x7E, 0xFF, 0xE1, 0xFF, 0xF7, 0xEC, 0xB2, 0xF0, 0x67, 0xFF, 0xFA, 0xEC, 0xB2,
|
||||||
|
0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xF2, 0x8B, 0xEC, 0xB2,
|
||||||
|
0xC2, 0x02, 0x41, 0x64, 0x74, 0xE9, 0x39, 0xE9, 0x39, 0xC1, 0x02, 0x41, 0x72, 0xF2, 0x43, 0xA2,
|
||||||
|
0x01, 0xB2, 0x7A, 0x68, 0xF1, 0xFA, 0xA0, 0x0D, 0x94, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD, 0xC2,
|
||||||
|
0x01, 0xB2, 0x6C, 0x72, 0xE7, 0x71, 0xE7, 0x71, 0xC1, 0x01, 0xB2, 0x73, 0xE9, 0x4F, 0xC1, 0x03,
|
||||||
|
0xE2, 0x6B, 0xEA, 0xB7, 0xC2, 0x01, 0xB2, 0x70, 0x6B, 0xFF, 0xFA, 0xE7, 0x5C, 0xC3, 0x01, 0xB2,
|
||||||
|
0x63, 0x6F, 0x72, 0xE7, 0x4F, 0xED, 0x8D, 0xE9, 0x31, 0x41, 0xBC, 0xE9, 0xE4, 0x41, 0x99, 0xFE,
|
||||||
|
0xBD, 0xA0, 0x0E, 0xD2, 0xC4, 0x03, 0xE2, 0xC4, 0x77, 0x66, 0x6D, 0xFF, 0xF9, 0xFF, 0xFD, 0xEA,
|
||||||
|
0x91, 0xEA, 0x91, 0xC1, 0x04, 0x32, 0x75, 0xE9, 0xA8, 0x21, 0x7A, 0xFA, 0xC7, 0x01, 0xB2, 0xC5,
|
||||||
|
0x63, 0x66, 0x6E, 0x74, 0x6F, 0x7A, 0xFF, 0xDD, 0xE9, 0x0B, 0xE9, 0x0B, 0xE9, 0x0B, 0xFF, 0xE8,
|
||||||
|
0xFF, 0xFD, 0xE7, 0x24, 0xA0, 0x0E, 0x53, 0x41, 0x6D, 0xE8, 0xB2, 0x21, 0x6F, 0xFC, 0x4B, 0x64,
|
||||||
|
0x66, 0x67, 0x68, 0x69, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0xE8, 0xAB, 0xE8, 0xAB, 0xE8, 0xAB,
|
||||||
|
0xE8, 0xAB, 0xFF, 0xF6, 0xE8, 0xAB, 0xE8, 0xAB, 0xE8, 0xAB, 0xE8, 0xAB, 0xFF, 0xFD, 0xE8, 0xAB,
|
||||||
|
0xC4, 0x01, 0xB2, 0x74, 0x63, 0x6D, 0x7A, 0xFF, 0xDE, 0xE6, 0xDC, 0xE6, 0xE0, 0xE6, 0xE0, 0xA0,
|
||||||
|
0x0E, 0xF2, 0x41, 0x75, 0xE6, 0x41, 0xC3, 0x01, 0xB2, 0x6D, 0x61, 0x77, 0xFF, 0xF9, 0xFF, 0xFC,
|
||||||
|
0xE6, 0xCA, 0xA0, 0x0E, 0xB2, 0x42, 0xBA, 0x9B, 0xFF, 0xFD, 0xE7, 0x18, 0xC2, 0x02, 0x92, 0x68,
|
||||||
|
0x7A, 0xE7, 0x11, 0xE7, 0x11, 0x41, 0xBA, 0xF3, 0x20, 0x21, 0xC5, 0xFC, 0xCF, 0x09, 0x12, 0xC5,
|
||||||
|
0x6F, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x70, 0x73, 0x74, 0x7A, 0xFF, 0xE9,
|
||||||
|
0xED, 0x2D, 0xEF, 0xF2, 0xFF, 0xF0, 0xFF, 0xFD, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07,
|
||||||
|
0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xEB, 0x0F, 0xC1, 0x01, 0x92, 0x72,
|
||||||
|
0xF0, 0x47, 0xC2, 0x01, 0x92, 0x7A, 0x68, 0xE5, 0xD6, 0xEA, 0xD9, 0xC1, 0x01, 0x92, 0x74, 0xEE,
|
||||||
|
0x23, 0xC2, 0x01, 0x92, 0x6D, 0x7A, 0xEA, 0xCA, 0xEA, 0xCA, 0xC2, 0x01, 0x92, 0x6D, 0x77, 0xE6,
|
||||||
|
0xB9, 0xEA, 0xC1, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70,
|
||||||
|
0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xA9, 0xED, 0xE1, 0xED, 0xE5, 0xFF, 0xD9, 0xFF, 0xDF, 0xFF,
|
||||||
|
0xE8, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xEE, 0x24, 0xFF,
|
||||||
|
0xEE, 0xE5, 0xC0, 0xE5, 0xC0, 0xFF, 0xF7, 0x42, 0x6B, 0x77, 0xE6, 0x7C, 0xE6, 0x7C, 0x21, 0x65,
|
||||||
|
0xF9, 0x22, 0x61, 0x69, 0xC2, 0xFD, 0x53, 0xC5, 0x64, 0xC4, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6E, 0xFB, 0xB1, 0xFE, 0x29, 0xE7, 0xB0,
|
||||||
|
0xE7, 0xCA, 0xFE, 0x89, 0xFE, 0x96, 0xE5, 0x99, 0xE7, 0xEC, 0xE5, 0x99, 0xFE, 0x99, 0xFE, 0xA2,
|
||||||
|
0xFE, 0xAE, 0xFE, 0xB7, 0xFE, 0xE6, 0xFF, 0x2A, 0xE8, 0x30, 0xE5, 0x99, 0xFF, 0x40, 0xFF, 0xFB,
|
||||||
|
0x41, 0x75, 0xEA, 0xA1, 0x42, 0x6E, 0x7A, 0xFF, 0xFC, 0xEA, 0x9D, 0x41, 0x75, 0xE6, 0x42, 0x21,
|
||||||
|
0x6E, 0xFC, 0x21, 0x77, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x43, 0x73, 0x77, 0x70, 0xEA,
|
||||||
|
0x86, 0xFF, 0xE9, 0xFF, 0xFD, 0x41, 0x6F, 0xFB, 0xC6, 0x21, 0x70, 0xFC, 0x21, 0x73, 0xFD, 0x21,
|
||||||
|
0x79, 0xFD, 0x21, 0x64, 0xFD, 0xC1, 0x01, 0x72, 0x6B, 0xE3, 0xDE, 0x44, 0x82, 0x9B, 0xBA, 0xBC,
|
||||||
|
0xFF, 0xFA, 0xE4, 0xFD, 0xE4, 0xFD, 0xE4, 0xFD, 0x43, 0x85, 0x99, 0x87, 0xEC, 0x11, 0xEC, 0x11,
|
||||||
|
0xE5, 0xE5, 0xA0, 0x0F, 0xB2, 0x21, 0x63, 0xFD, 0xA1, 0x04, 0x52, 0x79, 0xFD, 0x21, 0xBC, 0xFB,
|
||||||
|
0x21, 0xC5, 0xFD, 0xA1, 0x02, 0x72, 0x75, 0xFD, 0xA0, 0x0F, 0xD3, 0x44, 0xBA, 0x82, 0xBC, 0x9B,
|
||||||
|
0xFE, 0xA7, 0xFF, 0xF8, 0xFF, 0xFD, 0xE5, 0xC2, 0x41, 0x68, 0xE7, 0xD3, 0x21, 0x63, 0xFC, 0xA1,
|
||||||
|
0x02, 0x92, 0x75, 0xFD, 0xA0, 0x0F, 0x13, 0x21, 0x72, 0xFD, 0xA0, 0x10, 0x03, 0x21, 0x74, 0xFD,
|
||||||
|
0xA1, 0x0F, 0x13, 0x61, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x6F, 0xEC, 0xA3, 0x02, 0x92, 0x62, 0x73,
|
||||||
|
0x7A, 0xEC, 0xFA, 0xFD, 0x41, 0x74, 0xE7, 0xA7, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x41, 0x6B,
|
||||||
|
0xE7, 0x9D, 0x41, 0x6C, 0xE7, 0x99, 0xA0, 0x0F, 0x43, 0xA4, 0x02, 0x92, 0x61, 0x75, 0x79, 0x7A,
|
||||||
|
0xF2, 0xF5, 0xF9, 0xFD, 0xC1, 0x02, 0x92, 0x6D, 0xE7, 0x87, 0xA0, 0x10, 0x32, 0x21, 0x75, 0xFD,
|
||||||
|
0x21, 0x6B, 0xFD, 0xA1, 0x01, 0x71, 0x73, 0xFD, 0x41, 0x6D, 0xFF, 0x7A, 0x41, 0x6A, 0xFF, 0x76,
|
||||||
|
0x42, 0x72, 0x6F, 0xFF, 0x88, 0xFF, 0xFC, 0xC6, 0x01, 0x72, 0x61, 0x67, 0x69, 0x6A, 0x6C, 0x77,
|
||||||
|
0xFF, 0x6B, 0xFF, 0x81, 0xFF, 0xF1, 0xFF, 0x6B, 0xFF, 0x6B, 0xFF, 0xF9, 0x41, 0x6E, 0xE5, 0x37,
|
||||||
|
0x21, 0x6F, 0xFC, 0x41, 0x63, 0xE5, 0x30, 0x41, 0x70, 0xEF, 0xF5, 0x23, 0x67, 0x6B, 0x6C, 0xF5,
|
||||||
|
0xF8, 0xFC, 0x41, 0x7A, 0xE5, 0x21, 0x41, 0x72, 0xE5, 0x1D, 0x21, 0x65, 0xFC, 0x22, 0x67, 0x6D,
|
||||||
|
0xF5, 0xFD, 0xA0, 0x10, 0x73, 0x21, 0x77, 0xFD, 0x21, 0x99, 0xFD, 0x21, 0xC4, 0xFD, 0xC2, 0x02,
|
||||||
|
0x92, 0x69, 0x7A, 0xFF, 0xFD, 0xE4, 0xFF, 0xD9, 0x09, 0x12, 0xC4, 0xC5, 0x6D, 0x6F, 0x72, 0x75,
|
||||||
|
0x79, 0x7A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6E, 0x70,
|
||||||
|
0x73, 0x74, 0x77, 0xFF, 0x11, 0xFF, 0x34, 0xFF, 0x48, 0xFF, 0x64, 0xFF, 0x82, 0xFF, 0x8D, 0xFF,
|
||||||
|
0x9C, 0xFF, 0xB0, 0xFF, 0xD4, 0xE4, 0xFC, 0xED, 0xF8, 0xEE, 0x08, 0xFF, 0xE6, 0xE4, 0xFC, 0xF0,
|
||||||
|
0xB1, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xFF,
|
||||||
|
0xF7, 0xE4, 0xFC, 0xE4, 0xFC, 0xC1, 0x00, 0x71, 0x7A, 0xE2, 0x8E, 0xA1, 0x01, 0x92, 0x72, 0xFA,
|
||||||
|
0xC1, 0x00, 0x71, 0x72, 0xF0, 0x54, 0xA0, 0x10, 0x52, 0x21, 0x65, 0xFD, 0xC3, 0x10, 0x32, 0x69,
|
||||||
|
0x61, 0x77, 0xFF, 0xFD, 0xE7, 0xD2, 0xE7, 0xD2, 0x21, 0x77, 0xF4, 0xC3, 0x01, 0x92, 0x68, 0x69,
|
||||||
|
0x7A, 0xFF, 0xE5, 0xFF, 0xFD, 0xE8, 0x90, 0xA0, 0x0F, 0x74, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD,
|
||||||
|
0xC2, 0x01, 0x92, 0x6C, 0x72, 0xE8, 0x7B, 0xE8, 0x7B, 0xC1, 0x01, 0x92, 0x6B, 0xE8, 0x72, 0xC3,
|
||||||
|
0x01, 0x92, 0x63, 0x6D, 0x7A, 0xFA, 0x87, 0xE8, 0x6C, 0xE8, 0x6C, 0xC1, 0x00, 0x71, 0x61, 0xE3,
|
||||||
|
0x90, 0xC2, 0x01, 0x92, 0x6B, 0x72, 0xE8, 0x5A, 0xFF, 0xFA, 0x54, 0xC5, 0x64, 0xC4, 0x62, 0x63,
|
||||||
|
0x65, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0xFE,
|
||||||
|
0x51, 0xFF, 0x4D, 0xEB, 0x7A, 0xFF, 0xA1, 0xFF, 0xC1, 0xFF, 0xD3, 0xE3, 0x59, 0xFD, 0x72, 0xE3,
|
||||||
|
0x59, 0xFF, 0xD6, 0xE3, 0x59, 0xFF, 0xDF, 0xE3, 0x59, 0xFA, 0x70, 0xEB, 0xB4, 0xFF, 0xE5, 0xFF,
|
||||||
|
0xF7, 0xE3, 0x59, 0xE3, 0x59, 0xE3, 0x59, 0xC1, 0x01, 0x92, 0x72, 0xE8, 0x14, 0x52, 0xC4, 0xC5,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6F,
|
||||||
|
0xEB, 0x37, 0xEB, 0x3B, 0xFF, 0xFA, 0xEB, 0x48, 0xEB, 0x58, 0xE3, 0x16, 0xFD, 0x2F, 0xE3, 0x16,
|
||||||
|
0xE3, 0x16, 0xE3, 0x16, 0xFF, 0x9C, 0xFA, 0x2D, 0xEB, 0x71, 0xFA, 0x32, 0xFF, 0x9C, 0xE3, 0x16,
|
||||||
|
0xE3, 0x16, 0xEE, 0x5F, 0xC2, 0x00, 0x61, 0x65, 0x79, 0xFF, 0x86, 0xFF, 0xC9, 0xC3, 0x00, 0x51,
|
||||||
|
0x61, 0x65, 0x7A, 0xFD, 0xAE, 0xFD, 0xC5, 0xFF, 0xF7, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0xC3, 0x62,
|
||||||
|
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x76, 0x77, 0x78, 0x7A, 0xE3, 0x30, 0xF7, 0xEE, 0xF8, 0x4F, 0xE3, 0x44, 0xF8, 0x52, 0xE3,
|
||||||
|
0x44, 0xF8, 0xB1, 0xE3, 0x44, 0xE3, 0x44, 0xE3, 0x44, 0xF8, 0xED, 0xE3, 0x44, 0xE3, 0x44, 0xE3,
|
||||||
|
0x44, 0xE3, 0x44, 0xE3, 0x44, 0xFD, 0x4D, 0xE3, 0x44, 0xFF, 0xF4, 0xE3, 0x44, 0xE3, 0x44, 0xE3,
|
||||||
|
0x44, 0xE3, 0x44, 0xE3, 0x44, 0xE3, 0x44, 0x41, 0x73, 0xFE, 0x03, 0x21, 0x6E, 0xFC, 0x21, 0x61,
|
||||||
|
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x69, 0xF8, 0x07, 0x41, 0x65, 0xF8, 0x03, 0x21,
|
||||||
|
0x69, 0xFC, 0xA2, 0x02, 0x22, 0x67, 0x6E, 0xF5, 0xFD, 0xA0, 0x10, 0xA4, 0x21, 0x87, 0xFD, 0x24,
|
||||||
|
0x82, 0x9B, 0xBA, 0xBC, 0xFA, 0xFA, 0xFA, 0xFA, 0xA0, 0x10, 0xE4, 0xC2, 0x10, 0xE4, 0x68, 0x7A,
|
||||||
|
0xE7, 0x40, 0xE7, 0x40, 0xC2, 0x10, 0xE4, 0xC5, 0x7A, 0xEA, 0x7A, 0xE7, 0x37, 0xA0, 0x11, 0x24,
|
||||||
|
0xA1, 0x10, 0xE4, 0x7A, 0xFD, 0xC1, 0x10, 0xE4, 0x6F, 0xF7, 0xC9, 0xC1, 0x10, 0xE4, 0x63, 0xF9,
|
||||||
|
0x3B, 0xC2, 0x10, 0xE4, 0xC5, 0x7A, 0xEA, 0x79, 0xE7, 0x1A, 0xC2, 0x10, 0xE4, 0x63, 0x7A, 0xF9,
|
||||||
|
0x2C, 0xE7, 0x11, 0x21, 0x74, 0xDA, 0xD3, 0x02, 0x22, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67,
|
||||||
|
0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6A, 0x6E, 0xFF, 0xB6, 0xFF, 0xB9,
|
||||||
|
0xFF, 0xC2, 0xFF, 0xC5, 0xFF, 0xCE, 0xFF, 0xC2, 0xFF, 0xDA, 0xFF, 0xC2, 0xFF, 0xC2, 0xFF, 0xC2,
|
||||||
|
0xFF, 0xDF, 0xFF, 0xE5, 0xFF, 0xEB, 0xFF, 0xF4, 0xFF, 0xC2, 0xFF, 0xC2, 0xFF, 0xC2, 0xE2, 0x8A,
|
||||||
|
0xFF, 0xFD, 0x41, 0x9B, 0xE1, 0x9F, 0x41, 0x72, 0xED, 0x16, 0xC1, 0x02, 0x41, 0x72, 0xE1, 0x97,
|
||||||
|
0xCC, 0x02, 0x02, 0x61, 0x65, 0x75, 0xC5, 0x62, 0x64, 0x69, 0x6D, 0x6F, 0x70, 0x74, 0x77, 0xFF,
|
||||||
|
0x62, 0xFF, 0xB6, 0xE2, 0x40, 0xFF, 0xF2, 0xFF, 0xF6, 0xE2, 0x50, 0xE2, 0x50, 0xE2, 0x50, 0xE2,
|
||||||
|
0x50, 0xE4, 0xBC, 0xFF, 0xFA, 0xE2, 0x50, 0x21, 0x7A, 0xD9, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x76, 0x77, 0x78, 0x7A, 0xE1, 0xFF, 0xE2, 0x03, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xFF, 0x2A,
|
||||||
|
0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13,
|
||||||
|
0xFF, 0xFD, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13,
|
||||||
|
0xE2, 0x13, 0x41, 0x7A, 0xFC, 0xD8, 0x21, 0x6F, 0xFC, 0x45, 0x63, 0x6B, 0x70, 0x77, 0x72, 0xE1,
|
||||||
|
0x69, 0xE1, 0x4A, 0xE1, 0x4A, 0xE1, 0x4A, 0xFF, 0xFD, 0x21, 0x6F, 0xF0, 0x21, 0x6D, 0xFD, 0xC1,
|
||||||
|
0x03, 0x32, 0x73, 0xF7, 0x2C, 0x21, 0x6D, 0xFA, 0x42, 0x65, 0x6D, 0xFF, 0xFD, 0xF7, 0x38, 0x21,
|
||||||
|
0x64, 0xF9, 0x21, 0x65, 0xFD, 0xA0, 0x0E, 0x92, 0x21, 0x65, 0xFD, 0x21, 0xBC, 0xFD, 0x21, 0xC5,
|
||||||
|
0xFD, 0x21, 0x64, 0xFD, 0x21, 0x85, 0xFD, 0xC3, 0x00, 0x51, 0xC4, 0x6C, 0x72, 0xFF, 0xFD, 0xDF,
|
||||||
|
0x53, 0xDF, 0x53, 0x41, 0x62, 0xE7, 0xCB, 0xA0, 0x0D, 0x72, 0x21, 0x82, 0xFD, 0x21, 0xC5, 0xFD,
|
||||||
|
0x21, 0xB3, 0xFD, 0xC3, 0x06, 0x12, 0x7A, 0xC5, 0x64, 0xE8, 0x6A, 0xEA, 0xC4, 0xE8, 0x06, 0xC3,
|
||||||
|
0x05, 0xC2, 0x6E, 0x74, 0x7A, 0xED, 0x69, 0xED, 0x69, 0xE0, 0xB2, 0x51, 0x64, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xE8, 0xED,
|
||||||
|
0x59, 0xEA, 0x91, 0xE6, 0x36, 0xE9, 0xEB, 0xE6, 0x36, 0xE6, 0x36, 0xE6, 0x36, 0xE6, 0x36, 0xE6,
|
||||||
|
0x36, 0xE6, 0x36, 0xE6, 0x36, 0xFF, 0xF4, 0xE9, 0xFD, 0xE6, 0x36, 0xE6, 0x36, 0xE6, 0x36, 0xA2,
|
||||||
|
0x00, 0x51, 0xC3, 0x6F, 0xB1, 0xCC, 0xC5, 0x03, 0x32, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xDF, 0x6D,
|
||||||
|
0xDF, 0x6D, 0xDF, 0x6D, 0xDF, 0x6D, 0xDF, 0x6D, 0x21, 0x6F, 0xEE, 0x21, 0x65, 0xFD, 0x21, 0x72,
|
||||||
|
0xFD, 0xA1, 0x00, 0x51, 0x65, 0xFD, 0x41, 0x74, 0xE0, 0xFD, 0xC2, 0x02, 0x02, 0x69, 0x6F, 0xF7,
|
||||||
|
0x38, 0xFF, 0xFC, 0xA0, 0x11, 0x63, 0xA1, 0x04, 0x52, 0x72, 0xFD, 0x21, 0x74, 0xFB, 0x41, 0x6F,
|
||||||
|
0xE3, 0x6D, 0xA0, 0x11, 0x92, 0x21, 0x62, 0xFD, 0xA0, 0x11, 0xB2, 0x21, 0x74, 0xFD, 0x21, 0x75,
|
||||||
|
0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0xC5, 0x04, 0x32, 0x61, 0x69, 0x7A,
|
||||||
|
0x65, 0x6F, 0xFF, 0xE1, 0xFF, 0xE4, 0xFF, 0xEB, 0xE5, 0x41, 0xFF, 0xFD, 0x21, 0x72, 0xEE, 0x21,
|
||||||
|
0x65, 0xFD, 0x22, 0x62, 0x70, 0xB8, 0xFD, 0xA0, 0x11, 0xD2, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD,
|
||||||
|
0xA1, 0x11, 0xF3, 0x73, 0xFD, 0x21, 0x87, 0xFB, 0x41, 0x9B, 0xEE, 0x63, 0x21, 0xC5, 0xFC, 0x21,
|
||||||
|
0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x22, 0xC4, 0x63, 0xF0, 0xFD, 0x21, 0x9B, 0xFB, 0x42, 0xC5, 0x73,
|
||||||
|
0xFF, 0xFD, 0xF4, 0x53, 0xA1, 0x00, 0x51, 0x65, 0xF9, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x75, 0x76, 0x77, 0x78, 0x7A, 0xE0, 0x60, 0xE0, 0x64, 0xFE, 0xC3, 0xE0, 0x74, 0xF5, 0x82, 0xE0,
|
||||||
|
0x74, 0xE0, 0x74, 0xE0, 0x74, 0xE0, 0x74, 0xFE, 0xD9, 0xE0, 0x74, 0xFE, 0xEE, 0xE0, 0x74, 0xE0,
|
||||||
|
0x74, 0xE0, 0x74, 0xFE, 0xFA, 0xFF, 0x56, 0xE0, 0x74, 0xE0, 0x74, 0xFF, 0x78, 0xFF, 0xC9, 0xE0,
|
||||||
|
0x74, 0xE0, 0x74, 0xE0, 0x74, 0xFF, 0xFB, 0xA0, 0x12, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD,
|
||||||
|
0x21, 0x7A, 0xFD, 0x42, 0x6F, 0x72, 0xE4, 0x3A, 0xFF, 0xFD, 0x41, 0x61, 0xE9, 0x1C, 0x41, 0x7A,
|
||||||
|
0xE2, 0x9D, 0x21, 0x63, 0xFC, 0xC2, 0x04, 0x32, 0xC4, 0x65, 0xE2, 0x99, 0xFF, 0xFD, 0x21, 0x6A,
|
||||||
|
0xF7, 0x21, 0xB3, 0xFD, 0xC1, 0x04, 0x52, 0x75, 0xFA, 0x8E, 0x41, 0x65, 0xE2, 0x81, 0xC6, 0x04,
|
||||||
|
0x32, 0x65, 0x69, 0x79, 0x7A, 0x61, 0x6F, 0xFF, 0xF6, 0xFF, 0xFC, 0xE2, 0x7D, 0xE6, 0x8B, 0xE4,
|
||||||
|
0x6D, 0xE4, 0x6D, 0x21, 0x73, 0xEB, 0x21, 0x6E, 0xFD, 0x41, 0x68, 0xF5, 0x55, 0x21, 0x63, 0xFC,
|
||||||
|
0xA1, 0x00, 0x61, 0x65, 0xFD, 0xA3, 0x00, 0x51, 0xC3, 0x61, 0x7A, 0xCC, 0xF1, 0xFB, 0xA0, 0x12,
|
||||||
|
0x42, 0x21, 0x82, 0xFD, 0xA1, 0x02, 0xB1, 0xC5, 0xFD, 0xC3, 0x10, 0x32, 0x61, 0x65, 0x7A, 0xFB,
|
||||||
|
0x8D, 0xFB, 0x8D, 0xFF, 0xFB, 0x21, 0x63, 0xF4, 0x21, 0x85, 0xFD, 0x21, 0xC4, 0xFD, 0x21, 0x69,
|
||||||
|
0xFD, 0x21, 0x73, 0xFD, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
|
||||||
|
0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A,
|
||||||
|
0xDF, 0x85, 0xDF, 0x89, 0xFF, 0x7F, 0xDF, 0x99, 0xF4, 0xA7, 0xDF, 0x99, 0xE3, 0xBC, 0xDF, 0x99,
|
||||||
|
0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xFF, 0x86,
|
||||||
|
0xDF, 0x99, 0xFF, 0xD1, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xFF, 0xFD,
|
||||||
|
0xDF, 0x99, 0xA0, 0x04, 0xC2, 0x44, 0x9B, 0x82, 0xBA, 0xBC, 0xF0, 0x76, 0xFF, 0xFD, 0xFF, 0xFD,
|
||||||
|
0xFF, 0xFD, 0xC1, 0x0C, 0xB2, 0x77, 0xDE, 0x8F, 0x21, 0x7A, 0xFA, 0xC2, 0x0B, 0xE2, 0x65, 0x72,
|
||||||
|
0xFF, 0xFD, 0xF0, 0x60, 0xC2, 0x0B, 0xE2, 0x68, 0x7A, 0xF0, 0x57, 0xF0, 0x57, 0xA0, 0x06, 0x72,
|
||||||
|
0x42, 0xBA, 0xBC, 0xDF, 0x30, 0xDF, 0x30, 0xC3, 0x0B, 0xE2, 0x6B, 0xC5, 0x7A, 0xFF, 0xF6, 0xFF,
|
||||||
|
0xF9, 0xF0, 0x44, 0xA1, 0x0B, 0xE2, 0x6E, 0xEA, 0x21, 0x6A, 0xE5, 0x21, 0x65, 0xFD, 0xC2, 0x0B,
|
||||||
|
0xE2, 0x6C, 0x72, 0xFF, 0xFD, 0xF0, 0x2D, 0xA2, 0x0B, 0xE2, 0x73, 0x74, 0xD6, 0xD6, 0x21, 0x72,
|
||||||
|
0xCF, 0xC2, 0x0B, 0xE2, 0x62, 0x6B, 0xFF, 0xFD, 0xF0, 0x1A, 0xA2, 0x0B, 0xE2, 0x63, 0x64, 0xC3,
|
||||||
|
0xC3, 0xA0, 0x12, 0x63, 0x21, 0x73, 0xFD, 0xC3, 0x06, 0x12, 0x61, 0xC5, 0x64, 0xE7, 0xDF, 0xE8,
|
||||||
|
0x30, 0xE5, 0x72, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70,
|
||||||
|
0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xF4, 0xEA, 0xD1, 0xE8, 0x09, 0xE3, 0xAE, 0xE7, 0x63, 0xE3,
|
||||||
|
0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE7, 0x75, 0xE7,
|
||||||
|
0x75, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xC3, 0x0B, 0xE2, 0x70, 0x63, 0x6F, 0xFF, 0xBD, 0xF2,
|
||||||
|
0xBB, 0xFF, 0xCC, 0x41, 0xBC, 0xDE, 0x9D, 0x41, 0x7A, 0xF2, 0x59, 0xC4, 0x0B, 0xE2, 0x73, 0xC5,
|
||||||
|
0x6F, 0x7A, 0xFF, 0x62, 0xFF, 0xF8, 0xFF, 0xFC, 0xEF, 0xB0, 0xA0, 0x12, 0x92, 0xA0, 0x05, 0x91,
|
||||||
|
0x23, 0x6E, 0x63, 0x6B, 0xFA, 0xFD, 0xFD, 0xC3, 0x0B, 0xE2, 0x74, 0x63, 0x7A, 0xFF, 0xF9, 0xF2,
|
||||||
|
0x8B, 0xEF, 0x94, 0xC2, 0x0B, 0xE2, 0x6B, 0x72, 0xEF, 0x88, 0xEF, 0x88, 0x41, 0x65, 0xFF, 0x31,
|
||||||
|
0x41, 0x77, 0xE3, 0x31, 0xA2, 0x0B, 0xE2, 0x62, 0x65, 0xF8, 0xFC, 0x52, 0xC4, 0xC5, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xEF, 0x73,
|
||||||
|
0xFE, 0xFA, 0xFF, 0x10, 0xFF, 0x19, 0xFF, 0x2C, 0xFF, 0x38, 0xF2, 0x0C, 0xF2, 0x0C, 0xFF, 0x43,
|
||||||
|
0xFF, 0x4C, 0xFF, 0x56, 0xFF, 0x5F, 0xFF, 0xAC, 0xFF, 0xC0, 0xFF, 0xDC, 0xFF, 0xE8, 0xF2, 0x0C,
|
||||||
|
0xFF, 0xF9, 0xC1, 0x03, 0xA2, 0x6E, 0xDF, 0xC7, 0x43, 0x82, 0x9B, 0xBC, 0xFF, 0xFA, 0xDF, 0xBB,
|
||||||
|
0xDF, 0xBB, 0xC1, 0x01, 0xB2, 0x77, 0xDF, 0xF5, 0xC1, 0x01, 0xB2, 0x6B, 0xDE, 0x08, 0xC2, 0x01,
|
||||||
|
0xB2, 0x64, 0x74, 0xDF, 0xE9, 0xDF, 0xE9, 0xC1, 0x01, 0xB2, 0x63, 0xDD, 0xF5, 0xC6, 0x01, 0xB2,
|
||||||
|
0x62, 0x64, 0x6E, 0x73, 0x74, 0x7A, 0xDF, 0xDA, 0xDF, 0xDA, 0xDF, 0xDA, 0xDF, 0xDA, 0xDF, 0xDA,
|
||||||
|
0xDD, 0xF3, 0xA0, 0x12, 0xB3, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0xC3, 0x01, 0xB2, 0x70, 0x74,
|
||||||
|
0x7A, 0xE6, 0x1C, 0xFF, 0xFD, 0xDD, 0xD5, 0xA0, 0x12, 0xE2, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD,
|
||||||
|
0x21, 0x74, 0xFD, 0x21, 0x85, 0xFD, 0x21, 0xC4, 0xFD, 0xA1, 0x01, 0xB2, 0x6E, 0xFD, 0x41, 0x72,
|
||||||
|
0xDF, 0x99, 0x41, 0x82, 0xDF, 0x95, 0x21, 0xC5, 0xFC, 0xA2, 0x01, 0xB2, 0x62, 0x67, 0xF5, 0xFD,
|
||||||
|
0x52, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x77, 0x7A, 0xDF, 0x46, 0xFF, 0x88, 0xDD, 0x2F, 0xDF, 0x65, 0xDF, 0x75, 0xDD, 0x2F, 0xDD,
|
||||||
|
0x2F, 0xDD, 0x2F, 0xDF, 0x8A, 0xFF, 0x92, 0xFF, 0x98, 0xFF, 0x9E, 0xFF, 0xA7, 0xFF, 0xAD, 0xFF,
|
||||||
|
0xCB, 0xFF, 0x98, 0xFF, 0xE9, 0xFF, 0xF9, 0x43, 0xC5, 0x64, 0x6B, 0xDD, 0xC9, 0xDD, 0xCC, 0xDD,
|
||||||
|
0xCC, 0x41, 0x73, 0xF1, 0x96, 0x22, 0x6F, 0x75, 0xF2, 0xFC, 0x21, 0x6C, 0xFB, 0x41, 0x6D, 0xDD,
|
||||||
|
0xD0, 0x21, 0x6F, 0xFC, 0x21, 0x7A, 0xFD, 0x21, 0x63, 0xFD, 0x22, 0x65, 0x6C, 0xF0, 0xFD, 0x41,
|
||||||
|
0x62, 0xDD, 0xC4, 0x21, 0x65, 0xFC, 0xA1, 0x00, 0x51, 0x69, 0xFD, 0x41, 0x77, 0xDC, 0xCA, 0x21,
|
||||||
|
0x62, 0xFC, 0xC4, 0x13, 0x02, 0x69, 0x6F, 0x75, 0x77, 0xE0, 0xCC, 0xFF, 0xFD, 0xE0, 0xCC, 0xE0,
|
||||||
|
0xCC, 0x21, 0x82, 0xF1, 0x21, 0xC5, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0xC2, 0x0D, 0x72,
|
||||||
|
0x6F, 0x77, 0xDB, 0x56, 0xDB, 0x56, 0x21, 0x68, 0xF7, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0xA2,
|
||||||
|
0x00, 0x51, 0x70, 0x7A, 0xEB, 0xFD, 0x41, 0x7A, 0xDE, 0xA3, 0xA1, 0x03, 0xA2, 0x73, 0xFC, 0x44,
|
||||||
|
0x82, 0x9B, 0xBA, 0xBC, 0xDE, 0x94, 0xDE, 0x94, 0xDE, 0x94, 0xFF, 0xFB, 0x41, 0x61, 0xDE, 0x8D,
|
||||||
|
0xA1, 0x02, 0x41, 0x68, 0xFC, 0xC2, 0x01, 0xB2, 0x7A, 0x68, 0xFF, 0xFB, 0xDC, 0xDB, 0xC3, 0x01,
|
||||||
|
0xB2, 0xC5, 0x72, 0x7A, 0xDE, 0xA0, 0xDC, 0xD2, 0xDC, 0xD2, 0xC2, 0x01, 0xB2, 0x6C, 0x72, 0xDC,
|
||||||
|
0xC6, 0xDE, 0x80, 0xA0, 0x13, 0x23, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0xC2, 0x01, 0xB2, 0xC5,
|
||||||
|
0x7A, 0xDE, 0xBE, 0xDC, 0xB4, 0xC3, 0x01, 0xB2, 0x63, 0x6B, 0x72, 0xDC, 0xA7, 0xDC, 0xAB, 0xDC,
|
||||||
|
0xAB, 0xA0, 0x06, 0x13, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD, 0x41, 0x6F, 0xED, 0xB1, 0x21, 0x7A,
|
||||||
|
0xFC, 0x54, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6F, 0x70, 0x72,
|
||||||
|
0x73, 0x74, 0x77, 0x7A, 0x65, 0x69, 0xDE, 0x35, 0xFF, 0x9E, 0xDE, 0x4F, 0xFF, 0xB4, 0xFF, 0xBD,
|
||||||
|
0xDC, 0x1E, 0xDE, 0x71, 0xDC, 0x1E, 0xFF, 0xC9, 0xDC, 0x1E, 0xFE, 0x87, 0xFF, 0xD8, 0xDE, 0x90,
|
||||||
|
0xFF, 0xDB, 0xDE, 0xA9, 0xFF, 0xE4, 0xDC, 0x1E, 0xDC, 0x1E, 0xFF, 0xF6, 0xFF, 0xFD, 0xD9, 0x01,
|
||||||
|
0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xDC, 0x2B, 0xDC, 0x2F, 0xDC, 0x3F,
|
||||||
|
0xDC, 0x3F, 0xDC, 0x3F, 0xFE, 0xB2, 0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0x0C, 0xDC, 0x3F,
|
||||||
|
0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0x18, 0xE2, 0x93, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0x51,
|
||||||
|
0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0xC3, 0xDC, 0x3F, 0xC1, 0x00, 0x61, 0x6F,
|
||||||
|
0xE2, 0x3E, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0xFA, 0xD9, 0xB8, 0xD9, 0xB8, 0xD9, 0xB8,
|
||||||
|
0xD9, 0xB8, 0xA0, 0x13, 0x51, 0xA1, 0x13, 0x62, 0x75, 0xFD, 0x21, 0x87, 0xFB, 0x21, 0xC4, 0xFD,
|
||||||
|
0x21, 0x9B, 0xFD, 0x21, 0xC5, 0xFD, 0x41, 0x6F, 0xE7, 0x58, 0x21, 0x70, 0xFC, 0x21, 0x73, 0xFD,
|
||||||
|
0xC5, 0x01, 0xB2, 0xC5, 0x6F, 0x72, 0x79, 0x7A, 0xDD, 0x9E, 0xFF, 0xF3, 0xDB, 0xD0, 0xFF, 0xFD,
|
||||||
|
0xDB, 0xD0, 0xC3, 0x01, 0xB2, 0x74, 0x6C, 0x72, 0xDD, 0xA5, 0xDB, 0xBE, 0xDD, 0x78, 0xC3, 0x01,
|
||||||
|
0xB2, 0x67, 0x6B, 0x74, 0xDD, 0x99, 0xDD, 0x99, 0xDD, 0x99, 0x42, 0x72, 0x73, 0xFE, 0xE9, 0xFE,
|
||||||
|
0xE9, 0x21, 0x62, 0xF9, 0x41, 0x68, 0xDD, 0x83, 0xC3, 0x01, 0xB2, 0x63, 0xC5, 0x7A, 0xFF, 0xFC,
|
||||||
|
0xDD, 0xA2, 0xDB, 0x98, 0x41, 0x6F, 0xDA, 0xAF, 0xA1, 0x01, 0xB2, 0x74, 0xFC, 0xA0, 0x08, 0x41,
|
||||||
|
0x21, 0x63, 0xFD, 0xA1, 0x0C, 0xB2, 0x69, 0xFD, 0x42, 0x6E, 0x7A, 0xFF, 0xFB, 0xFE, 0xE2, 0x41,
|
||||||
|
0x64, 0xDB, 0xD4, 0x21, 0x65, 0xFC, 0x21, 0x69, 0xFD, 0x55, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||||
|
0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0x69, 0x6E, 0xDD,
|
||||||
|
0x0D, 0xE6, 0x77, 0xDD, 0x27, 0xDD, 0x2C, 0xFF, 0x97, 0xDA, 0xF6, 0xDD, 0x49, 0xDA, 0xF6, 0xFF,
|
||||||
|
0xA9, 0xFF, 0xB5, 0xDD, 0x63, 0xFF, 0xC8, 0xDA, 0xF6, 0xFF, 0xCF, 0xDD, 0x81, 0xDD, 0x8D, 0xFF,
|
||||||
|
0xDF, 0xDA, 0xF6, 0xDA, 0xF6, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x13, 0x82, 0x21, 0x75, 0xFD, 0x21,
|
||||||
|
0x77, 0xFD, 0x22, 0x6C, 0x72, 0xF7, 0xF7, 0xA3, 0x0C, 0xB2, 0x61, 0x65, 0x79, 0xF8, 0xFB, 0xF2,
|
||||||
|
0x21, 0x7A, 0xF7, 0x41, 0x74, 0xEE, 0xCD, 0x21, 0x6E, 0xFC, 0x41, 0x64, 0xEE, 0xC6, 0x21, 0x65,
|
||||||
|
0xFC, 0x41, 0x6C, 0xEE, 0xBF, 0x21, 0x61, 0xFC, 0x23, 0x6F, 0x72, 0x77, 0xEF, 0xF6, 0xFD, 0x21,
|
||||||
|
0x6B, 0xF9, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0x51, 0x65, 0x79, 0xDB, 0xFD, 0xC1, 0x01, 0xB2, 0x6C,
|
||||||
|
0xDA, 0xE4, 0xC3, 0x01, 0xB2, 0x6B, 0xC5, 0x7A, 0xDC, 0xC5, 0xDC, 0xE8, 0xDA, 0xDE, 0xC3, 0x01,
|
||||||
|
0xB2, 0x63, 0x6D, 0x7A, 0xDA, 0xCE, 0xFF, 0x4F, 0xDA, 0xD2, 0xA0, 0x13, 0xA5, 0x21, 0x73, 0xFD,
|
||||||
|
0x42, 0x65, 0x70, 0xFF, 0xFD, 0xDC, 0xA7, 0x21, 0x6C, 0xF3, 0x21, 0x65, 0xFD, 0x22, 0x6D, 0x73,
|
||||||
|
0xF3, 0xFD, 0x41, 0x70, 0xDC, 0x95, 0x21, 0x61, 0xFC, 0xC3, 0x01, 0xB2, 0x6B, 0x65, 0x68, 0xDC,
|
||||||
|
0x4A, 0xFF, 0xF4, 0xFF, 0xFD, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xDC, 0x41, 0xE5, 0xAB, 0xDA, 0x2A, 0xDC, 0x60, 0xDC,
|
||||||
|
0x70, 0xDA, 0x2A, 0xDA, 0x2A, 0xDA, 0x2A, 0xFF, 0xB7, 0xDA, 0x2A, 0xDA, 0x2A, 0xFC, 0xA2, 0xFF,
|
||||||
|
0xBD, 0xFF, 0xC9, 0xFF, 0xF4, 0xDA, 0x2A, 0xDA, 0x2A, 0x41, 0x6B, 0xEE, 0x58, 0x21, 0x6F, 0xFC,
|
||||||
|
0x21, 0x6E, 0xFD, 0x21, 0x6D, 0xFD, 0xC1, 0x13, 0xF2, 0x77, 0xE7, 0xE4, 0x21, 0x68, 0xFA, 0x21,
|
||||||
|
0x63, 0xFD, 0x21, 0x79, 0xFD, 0x21, 0x77, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x72, 0xFD, 0xA1, 0x00,
|
||||||
|
0x51, 0x61, 0xFD, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9,
|
||||||
|
0xA5, 0xD9, 0xA5, 0xA0, 0x14, 0x12, 0x42, 0xBA, 0xBC, 0xFF, 0xFD, 0xD8, 0x6D, 0xC2, 0x01, 0x92,
|
||||||
|
0xC5, 0x7A, 0xFF, 0xF9, 0xDE, 0x8E, 0xC1, 0x01, 0x92, 0x63, 0xE1, 0xD8, 0x52, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xE1,
|
||||||
|
0xA8, 0xFF, 0xD7, 0xD9, 0x87, 0xE1, 0xB9, 0xFF, 0xF1, 0xD9, 0x87, 0xD9, 0x87, 0xD9, 0x87, 0xD9,
|
||||||
|
0x87, 0xD9, 0x87, 0xFF, 0xFA, 0xD9, 0x87, 0xD9, 0x87, 0xE1, 0xEB, 0xE1, 0xEB, 0xD9, 0x87, 0xD9,
|
||||||
|
0x87, 0xD9, 0x87, 0x21, 0x65, 0xC9, 0xA1, 0x00, 0x51, 0x69, 0xFD, 0xC1, 0x0C, 0xB2, 0x75, 0xFE,
|
||||||
|
0xAE, 0x21, 0x7A, 0xFA, 0xA1, 0x00, 0x51, 0x6F, 0xFD, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62,
|
||||||
|
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x76, 0x77, 0x78, 0x7A, 0xD9, 0xA0, 0xFD, 0xC9, 0xFE, 0x60, 0xD9, 0xB4, 0xD9, 0xB4, 0xFE,
|
||||||
|
0xDC, 0xFF, 0x2C, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9, 0xB4, 0xFF, 0x6A, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9,
|
||||||
|
0xB4, 0xFF, 0x85, 0xFF, 0xED, 0xDD, 0xD7, 0xD9, 0xB4, 0xFF, 0xFB, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9,
|
||||||
|
0xB4, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9, 0xB4, 0x5A, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
||||||
|
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
|
||||||
|
0x78, 0x7A, 0xD7, 0x77, 0xD8, 0x78, 0xD9, 0x3F, 0xD9, 0xA7, 0xDA, 0x7B, 0xDC, 0x80, 0xDD, 0x34,
|
||||||
|
0xDD, 0x44, 0xDD, 0x9C, 0xDD, 0xFC, 0xDE, 0x6F, 0xDE, 0x83, 0xDF, 0x6B, 0xDF, 0xBD, 0xE0, 0x1E,
|
||||||
|
0xEA, 0x49, 0xED, 0xC7, 0xF6, 0x22, 0xF7, 0x53, 0xF8, 0xF2, 0xF9, 0xCD, 0xFB, 0x24, 0xDD, 0x44,
|
||||||
|
0xFD, 0x27, 0xDD, 0x44, 0xFF, 0xB2, 0xA0, 0x14, 0x31, 0xA0, 0x15, 0x81, 0xA1, 0x14, 0x62, 0x2E,
|
||||||
|
0xFD, 0x21, 0x2E, 0xF8, 0x25, 0x84, 0x9B, 0xBA, 0xBC, 0x82, 0xF8, 0xF8, 0xF8, 0xF8, 0xFD, 0xA1,
|
||||||
|
0x15, 0x42, 0x2E, 0xEA, 0x21, 0x87, 0xFB, 0x36, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D,
|
||||||
|
0x6E, 0x70, 0x73, 0x74, 0x7A, 0xC4, 0x2E, 0x68, 0x6A, 0x6C, 0x72, 0x76, 0x77, 0x78, 0xED, 0xE5,
|
||||||
|
0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xFD, 0xE2, 0xEA, 0xEA, 0xEA,
|
||||||
|
0xEA, 0xEA, 0xEA, 0xEA, 0x43, 0x85, 0x99, 0x87, 0xFF, 0xB2, 0xFF, 0xB2, 0xFF, 0xD3, 0x41, 0x7A,
|
||||||
|
0xF9, 0x9F, 0x21, 0x63, 0xFC, 0xA1, 0x14, 0x31, 0x77, 0xFD, 0x21, 0xB3, 0xFB, 0xA0, 0x14, 0x42,
|
||||||
|
0xC2, 0x14, 0x31, 0x69, 0x2E, 0xFF, 0xFD, 0xFF, 0x99, 0xA0, 0x14, 0x61, 0x42, 0x63, 0x2E, 0xFF,
|
||||||
|
0xFD, 0xFF, 0x8D, 0x21, 0x87, 0xF9, 0x45, 0x9B, 0xBA, 0xBC, 0x82, 0x84, 0xFF, 0x86, 0xFF, 0x86,
|
||||||
|
0xFF, 0x86, 0xFF, 0x8B, 0xFF, 0x8B, 0x43, 0x6D, 0x6E, 0x2E, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0x73,
|
||||||
|
0x56, 0x63, 0xC4, 0xC5, 0x62, 0x64, 0x66, 0x67, 0x6B, 0x70, 0x73, 0x74, 0x7A, 0x6C, 0x2E, 0x68,
|
||||||
|
0x6A, 0x6D, 0x6E, 0x72, 0x76, 0x77, 0x78, 0xFF, 0xD0, 0xFF, 0xE3, 0xFF, 0xE6, 0xFF, 0x6C, 0xFF,
|
||||||
|
0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF,
|
||||||
|
0xF6, 0xFF, 0x69, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF,
|
||||||
|
0x71, 0xFF, 0x71, 0xA0, 0x00, 0xF1, 0x21, 0xBA, 0xFD, 0xC2, 0x14, 0x62, 0xC5, 0x2E, 0xFF, 0xFD,
|
||||||
|
0xFF, 0x20, 0x41, 0x87, 0xFF, 0x1A, 0x45, 0x9B, 0xBC, 0xBA, 0x82, 0x84, 0xFF, 0x16, 0xFF, 0x16,
|
||||||
|
0xFF, 0x29, 0xFF, 0x1B, 0xFF, 0x1B, 0x56, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0x2E, 0x68, 0x6A, 0x72, 0x76, 0x78, 0xFF, 0xE3, 0xFF,
|
||||||
|
0xEC, 0xFF, 0xF0, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF,
|
||||||
|
0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x03, 0xFF,
|
||||||
|
0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0x45, 0x84, 0x9B, 0xBA, 0xBC, 0x82, 0xFE,
|
||||||
|
0xC3, 0xFE, 0xC3, 0xFE, 0xC3, 0xFE, 0xC3, 0xFE, 0xD6, 0x56, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||||
|
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x2E, 0x76, 0x78,
|
||||||
|
0xFF, 0x99, 0xFF, 0xF0, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3,
|
||||||
|
0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3,
|
||||||
|
0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB0, 0xFE, 0xB8, 0xFE, 0xB8, 0x45, 0x82, 0x84, 0x9B,
|
||||||
|
0xBA, 0xBC, 0xFE, 0x70, 0xFE, 0x70, 0xFE, 0x70, 0xFE, 0x70, 0xFE, 0x70, 0x56, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A,
|
||||||
|
0x2E, 0x76, 0x78, 0xFF, 0x46, 0xFF, 0xF0, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE,
|
||||||
|
0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE,
|
||||||
|
0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x5D, 0xFE, 0x65, 0xFE, 0x65, 0x45,
|
||||||
|
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFE, 0x1D, 0xFE, 0x1D, 0xFE, 0x1D, 0xFE, 0x1D, 0xFE, 0x30, 0x56,
|
||||||
|
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x77, 0x7A, 0x2E, 0x68, 0x76, 0x78, 0xFE, 0xF3, 0xFF, 0xF0, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D,
|
||||||
|
0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D,
|
||||||
|
0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0A, 0xFE, 0x12, 0xFE, 0x12,
|
||||||
|
0xFE, 0x12, 0x45, 0x9B, 0xBA, 0x82, 0x84, 0xBC, 0xFE, 0x5E, 0xFE, 0xC4, 0xFF, 0x17, 0xFF, 0x6A,
|
||||||
|
0xFF, 0xBD, 0x42, 0x6B, 0x2E, 0xFE, 0x27, 0xFD, 0xB7, 0xC1, 0x14, 0x82, 0x2E, 0xFD, 0xB0, 0x25,
|
||||||
|
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xF3, 0xFA, 0xFA, 0xFA, 0xFA, 0x21, 0x87, 0xEF, 0xA0, 0x16, 0x43,
|
||||||
|
0x21, 0x79, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x77, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x61, 0xFD, 0x45,
|
||||||
|
0x6E, 0x2E, 0x7A, 0x64, 0x6F, 0xFD, 0xFA, 0xFD, 0x8A, 0xFD, 0x92, 0xFE, 0x64, 0xFF, 0xFD, 0xC1,
|
||||||
|
0x15, 0x62, 0x2E, 0xFD, 0x7A, 0xA0, 0x16, 0x22, 0x21, 0x7A, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x61,
|
||||||
|
0xFD, 0x21, 0x68, 0xFD, 0x21, 0x6B, 0xFD, 0x42, 0x2E, 0x6F, 0xFD, 0x62, 0xFF, 0xFD, 0x41, 0x68,
|
||||||
|
0xFE, 0x35, 0x21, 0x74, 0xFC, 0xA0, 0x15, 0x93, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x64,
|
||||||
|
0xD9, 0xF0, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x68, 0xFD, 0x23, 0x65, 0x66, 0x6B, 0xE7,
|
||||||
|
0xF0, 0xFD, 0xA0, 0x04, 0xD3, 0x21, 0x75, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x0B,
|
||||||
|
0xB2, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x7A, 0xFD, 0xA0, 0x15, 0xC4,
|
||||||
|
0xA1, 0x01, 0x22, 0x6D, 0xFD, 0x21, 0x73, 0xFB, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x16,
|
||||||
|
0x02, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6E, 0xFD,
|
||||||
|
0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x24, 0x6E, 0x73, 0x7A, 0x72, 0xC5, 0xD4, 0xE5, 0xFD, 0x41,
|
||||||
|
0x61, 0xDB, 0x01, 0x21, 0x6D, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xB5, 0x21, 0x6D, 0xFD, 0xA0,
|
||||||
|
0x13, 0x02, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x21, 0x73, 0xFB, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
|
||||||
|
0x21, 0x6E, 0xFD, 0x22, 0x7A, 0x69, 0xE9, 0xFD, 0xA0, 0x16, 0x72, 0x21, 0x6E, 0xFD, 0x21, 0x61,
|
||||||
|
0xFD, 0x21, 0x70, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x73, 0x6B, 0xE9, 0xFD, 0x5A,
|
||||||
|
0xC5, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x72, 0x62, 0x2E,
|
||||||
|
0x68, 0x6A, 0x6C, 0x76, 0x77, 0x78, 0x65, 0x69, 0x6F, 0x75, 0xFF, 0x00, 0xFF, 0x0B, 0xFE, 0xFA,
|
||||||
|
0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA,
|
||||||
|
0xFE, 0xFA, 0xFE, 0xFA, 0xFF, 0x20, 0xFF, 0x30, 0xFC, 0xAA, 0xFC, 0xB2, 0xFC, 0xB2, 0xFF, 0x48,
|
||||||
|
0xFC, 0xB2, 0xFC, 0xB2, 0xFC, 0xB2, 0xFF, 0x6C, 0xFF, 0xB7, 0xFF, 0xC7, 0xFF, 0xFB, 0x45, 0x84,
|
||||||
|
0x9B, 0xBA, 0xBC, 0x82, 0xFE, 0xAB, 0xFE, 0xAB, 0xFE, 0xAB, 0xFE, 0xAB, 0xFC, 0x63, 0xA0, 0x14,
|
||||||
|
0x62, 0x21, 0x87, 0xFD, 0xC1, 0x00, 0xF1, 0x2E, 0xFC, 0x45, 0x25, 0x84, 0x9B, 0xBA, 0xBC, 0x82,
|
||||||
|
0xF4, 0xF4, 0xF4, 0xF4, 0xFA, 0xC1, 0x00, 0xF1, 0x7A, 0xFC, 0x3C, 0xA0, 0x16, 0x91, 0x21, 0x74,
|
||||||
|
0xFD, 0x21, 0x73, 0xFD, 0xD4, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D,
|
||||||
|
0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0x72, 0x77, 0x6A, 0x6C, 0x75, 0xFF, 0xDD, 0xFF, 0xE6, 0xFF,
|
||||||
|
0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF,
|
||||||
|
0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFC, 0x25, 0xFF, 0xF1, 0xFF, 0xE0, 0xFC, 0xFF, 0xFC,
|
||||||
|
0xFF, 0xFF, 0xFD, 0x44, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0x9B, 0xFF, 0x9B, 0xFF, 0x9B, 0xFF, 0x9B,
|
||||||
|
0xD0, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73,
|
||||||
|
0x74, 0x7A, 0x2E, 0xFF, 0x91, 0xFF, 0xF3, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF,
|
||||||
|
0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFB, 0xDC, 0xFF,
|
||||||
|
0x8E, 0xFB, 0xD9, 0x41, 0x6C, 0xFE, 0x32, 0x21, 0x6C, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x77, 0xFD,
|
||||||
|
0x21, 0x64, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x16, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
|
0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x58, 0xC4, 0xC5, 0x62, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x73, 0x74, 0x68, 0x7A, 0x63, 0x2E, 0x6A, 0x72, 0x76, 0x77, 0x78, 0x61, 0x75, 0xFD, 0xE5,
|
||||||
|
0xFF, 0x29, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4,
|
||||||
|
0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFF, 0x5F, 0xFF, 0xAB, 0xFE, 0x0A, 0xFB, 0x84,
|
||||||
|
0xFB, 0x8C, 0xFB, 0x8C, 0xFB, 0x8C, 0xFB, 0x8C, 0xFB, 0x8C, 0xFF, 0xEE, 0xFF, 0xFD, 0x41, 0x7A,
|
||||||
|
0xFB, 0xAB, 0x43, 0x62, 0x73, 0x2E, 0xFB, 0xA7, 0xFF, 0xFC, 0xFB, 0x37, 0xA0, 0x14, 0xA2, 0x21,
|
||||||
|
0x87, 0xFD, 0x24, 0x84, 0x9B, 0xBA, 0xBC, 0xFA, 0xFA, 0xFA, 0xFA, 0xCF, 0x02, 0x31, 0xC4, 0xC5,
|
||||||
|
0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0xFF, 0xF4, 0xFF,
|
||||||
|
0xF7, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF,
|
||||||
|
0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFB, 0x1E, 0x45, 0x84, 0x9B, 0xBA, 0xBC,
|
||||||
|
0x82, 0xFF, 0xC1, 0xFF, 0xC1, 0xFF, 0xC1, 0xFF, 0xC1, 0xE7, 0xF3, 0x42, 0x62, 0x6D, 0xDF, 0xFD,
|
||||||
|
0xDF, 0xFD, 0x21, 0x7A, 0xF9, 0xD4, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B,
|
||||||
|
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0x6A, 0x6C, 0x72, 0x77, 0x65, 0xFF, 0xAA, 0xFF, 0xE6,
|
||||||
|
0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7,
|
||||||
|
0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFA, 0xD4, 0xE7, 0xD9, 0xE7, 0xD9, 0xE7, 0xD9,
|
||||||
|
0xE7, 0xD9, 0xFF, 0xFD, 0x45, 0x82, 0xBA, 0xBC, 0x84, 0x9B, 0xFF, 0x5E, 0xFF, 0x77, 0xFF, 0xC1,
|
||||||
|
0xFC, 0xE5, 0xFC, 0xE5, 0xA0, 0x14, 0xC2, 0x21, 0x77, 0xFD, 0x21, 0x6F, 0xFD, 0xC2, 0x14, 0x82,
|
||||||
|
0x69, 0x2E, 0xFF, 0xFD, 0xFA, 0x7C, 0x42, 0x2E, 0x77, 0xFA, 0x73, 0xE7, 0x78, 0x43, 0x6E, 0x2E,
|
||||||
|
0x7A, 0xFA, 0xDC, 0xFA, 0x6C, 0xFF, 0xF9, 0xD0, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||||
|
0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0xFE, 0x1A, 0xFE, 0x7C, 0xFE, 0x17,
|
||||||
|
0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17,
|
||||||
|
0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFA, 0x62, 0x41, 0x64, 0xDF, 0x60, 0x21, 0x6E,
|
||||||
|
0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
|
||||||
|
0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFC, 0xE8, 0x21, 0x73, 0xFC, 0x21, 0x61, 0xFD,
|
||||||
|
0x21, 0x70, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x58, 0xC5, 0xC4, 0x62, 0x63, 0x66, 0x67,
|
||||||
|
0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x72, 0x7A, 0x64, 0x2E, 0x68, 0x6A, 0x6C, 0x76, 0x77, 0x78,
|
||||||
|
0x65, 0x75, 0xFF, 0x6B, 0xFC, 0x61, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50,
|
||||||
|
0xFC, 0x50, 0xFF, 0x84, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50, 0xFF, 0x94, 0xFF, 0x9E, 0xFC, 0x86,
|
||||||
|
0xFA, 0x00, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFF, 0xEA,
|
||||||
|
0xFF, 0xFD, 0x41, 0x87, 0xF9, 0xBF, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xF9, 0xBB, 0xF9, 0xBB,
|
||||||
|
0xF9, 0xBB, 0xF9, 0xBB, 0xF9, 0xBB, 0x41, 0x6D, 0xD7, 0xBA, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD,
|
||||||
|
0x41, 0x6E, 0xFC, 0x6E, 0x21, 0x69, 0xFC, 0x21, 0x62, 0xFD, 0x22, 0x68, 0x7A, 0xF3, 0xFD, 0x21,
|
||||||
|
0x73, 0xFB, 0xA0, 0x16, 0xC2, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x7A, 0xFD, 0x41, 0x72,
|
||||||
|
0xFC, 0x07, 0x21, 0x6F, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6B, 0xFD, 0xA0, 0x16, 0xE3, 0x21, 0x74,
|
||||||
|
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x66, 0xFD, 0x44, 0x6B, 0x72,
|
||||||
|
0x6C, 0x73, 0xD1, 0x98, 0xFF, 0xDE, 0xFF, 0xEB, 0xFF, 0xFD, 0x58, 0x63, 0x6B, 0x6D, 0x6E, 0x66,
|
||||||
|
0x2E, 0xC4, 0xC5, 0x62, 0x64, 0x67, 0x68, 0x6A, 0x6C, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||||
|
0x7A, 0x69, 0x6F, 0xFB, 0x9F, 0xFB, 0x9F, 0xFB, 0x9F, 0xFB, 0x9F, 0xFB, 0xD5, 0xF9, 0x4F, 0xFF,
|
||||||
|
0x98, 0xFF, 0x9C, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9,
|
||||||
|
0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xFF,
|
||||||
|
0xC5, 0xFF, 0xF3, 0x42, 0x62, 0x2E, 0xF9, 0x76, 0xF9, 0x06, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC,
|
||||||
|
0xFF, 0xF9, 0xFB, 0x4F, 0xFB, 0x4F, 0xFB, 0x4F, 0xFB, 0x4F, 0xA0, 0x17, 0x12, 0x21, 0x6F, 0xFD,
|
||||||
|
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x74, 0xF4, 0x52,
|
||||||
|
0x43, 0x2E, 0x61, 0x6F, 0xF8, 0xD9, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xE5, 0xAD, 0x21, 0x65,
|
||||||
|
0xFC, 0x41, 0x61, 0xD6, 0xC0, 0x21, 0x74, 0xFC, 0x21, 0x70, 0xFD, 0x22, 0x67, 0x6F, 0xF3, 0xFD,
|
||||||
|
0x21, 0x64, 0xFB, 0xC1, 0x02, 0x02, 0x7A, 0xD1, 0x1E, 0x21, 0x73, 0xFA, 0x21, 0x66, 0xFD, 0x21,
|
||||||
|
0x6C, 0xFD, 0x58, 0xC5, 0xC4, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x70, 0x73, 0x74, 0x7A, 0x67,
|
||||||
|
0x2E, 0x68, 0x6A, 0x6C, 0x6E, 0x72, 0x76, 0x77, 0x78, 0x61, 0x6F, 0xFF, 0xA8, 0xFB, 0x08, 0xFA,
|
||||||
|
0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA,
|
||||||
|
0xF7, 0xFA, 0xF7, 0xFB, 0x2D, 0xF8, 0xA7, 0xF8, 0xAF, 0xF8, 0xAF, 0xF8, 0xAF, 0xF8, 0xAF, 0xFF,
|
||||||
|
0xCE, 0xF8, 0xAF, 0xF8, 0xAF, 0xF8, 0xAF, 0xFF, 0xEE, 0xFF, 0xFD, 0x45, 0x82, 0x84, 0x9B, 0xBA,
|
||||||
|
0xBC, 0xFA, 0xAE, 0xFA, 0xAE, 0xFA, 0xAE, 0xFA, 0xAE, 0xFA, 0xAE, 0x41, 0x7A, 0xFB, 0x26, 0x21,
|
||||||
|
0x73, 0xFC, 0xA0, 0x17, 0x32, 0x21, 0x77, 0xFD, 0x21, 0x7A, 0xFD, 0x41, 0x79, 0xD6, 0x55, 0x21,
|
||||||
|
0x65, 0xFC, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x78, 0xF3, 0xFD, 0x58, 0xC4, 0xC5, 0x62, 0x63, 0x64,
|
||||||
|
0x66, 0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x68, 0x2E, 0x76,
|
||||||
|
0x78, 0x69, 0x75, 0xFA, 0x90, 0xFF, 0xD1, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA,
|
||||||
|
0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA,
|
||||||
|
0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0xB5, 0xF8, 0x2F, 0xF8, 0x37, 0xF8, 0x37, 0xFF,
|
||||||
|
0xE5, 0xFF, 0xFB, 0xA0, 0x05, 0x42, 0x21, 0x82, 0xFD, 0xC3, 0x14, 0x82, 0x2E, 0xC5, 0x72, 0xF7,
|
||||||
|
0xE0, 0xFF, 0xFD, 0xFF, 0xFA, 0xA0, 0x17, 0x53, 0x22, 0x62, 0x6D, 0xFD, 0xFD, 0x21, 0x7A, 0xFB,
|
||||||
|
0x21, 0x7A, 0xFD, 0x57, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6A, 0x2E, 0x76, 0x78, 0x61, 0xFA, 0x27, 0xFF, 0x68, 0xFA,
|
||||||
|
0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA,
|
||||||
|
0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFF, 0xE6, 0xFA, 0x16, 0xFA, 0x16, 0xFA,
|
||||||
|
0x4C, 0xF7, 0xC6, 0xF7, 0xCE, 0xF7, 0xCE, 0xFF, 0xFD, 0xA0, 0x14, 0xE3, 0xA1, 0x14, 0x82, 0x74,
|
||||||
|
0xFD, 0xC3, 0x14, 0x82, 0x7A, 0x2E, 0x74, 0xFF, 0xFB, 0xF7, 0x78, 0xF7, 0x80, 0x41, 0x6E, 0xFE,
|
||||||
|
0x7D, 0x21, 0x6F, 0xFC, 0x21, 0x72, 0xFD, 0x41, 0x65, 0xFE, 0x73, 0x21, 0x68, 0xFC, 0x21, 0x75,
|
||||||
|
0xFD, 0x22, 0x6B, 0x72, 0xF3, 0xFD, 0x21, 0x73, 0xFB, 0x21, 0x6C, 0xFD, 0x21, 0x72, 0xFD, 0x41,
|
||||||
|
0x66, 0xE4, 0x4F, 0x21, 0x66, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x68, 0xFD, 0x21,
|
||||||
|
0x63, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x6D, 0xE6, 0x76, 0x21, 0x73, 0xFC, 0x21, 0x65, 0xFD, 0x21,
|
||||||
|
0x72, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6E, 0xFD, 0x59, 0xC5, 0xC4, 0x62, 0x63, 0x64, 0x66, 0x67,
|
||||||
|
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x6B, 0x2E, 0x68, 0x6A, 0x6C, 0x72, 0x76, 0x77, 0x78, 0x61,
|
||||||
|
0x69, 0x6F, 0xFE, 0x22, 0xF9, 0x82, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71,
|
||||||
|
0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71, 0xFF, 0xA9, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0xA7, 0xF7, 0x21,
|
||||||
|
0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xFF, 0xD4,
|
||||||
|
0xFF, 0xEA, 0xFF, 0xFD, 0xA0, 0x14, 0xB1, 0x21, 0x77, 0xFD, 0x21, 0x64, 0xFD, 0x41, 0x66, 0xF9,
|
||||||
|
0xEC, 0x21, 0x66, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x77, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x72, 0xFE,
|
||||||
|
0x75, 0x21, 0x65, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x66, 0x6B, 0xF0, 0xFD, 0x41,
|
||||||
|
0x6F, 0xF7, 0x84, 0x59, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6C, 0x2E, 0x76, 0x78, 0x65, 0x75, 0x79, 0xF9, 0x07, 0xFE,
|
||||||
|
0x48, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8,
|
||||||
|
0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8,
|
||||||
|
0xF6, 0xF9, 0x2C, 0xF6, 0xA6, 0xF6, 0xAE, 0xF6, 0xAE, 0xFF, 0xD7, 0xFF, 0xF7, 0xFF, 0xFC, 0xA0,
|
||||||
|
0x15, 0x13, 0xC2, 0x14, 0x82, 0x6E, 0x2E, 0xFF, 0xFD, 0xF6, 0x57, 0x41, 0x74, 0xFB, 0xC9, 0x41,
|
||||||
|
0x6A, 0xFB, 0xC5, 0x22, 0x73, 0x7A, 0xF8, 0xFC, 0xC2, 0x14, 0x82, 0x65, 0x2E, 0xFF, 0xFB, 0xF6,
|
||||||
|
0x41, 0x41, 0x6E, 0xFE, 0xB8, 0xC3, 0x14, 0x82, 0x6B, 0x2E, 0x74, 0xFF, 0xFC, 0xF6, 0x34, 0xF6,
|
||||||
|
0x3C, 0x41, 0x82, 0xFC, 0xA1, 0x43, 0xC5, 0x6C, 0x6E, 0xFF, 0xFC, 0xFC, 0x9D, 0xFC, 0x9D, 0x21,
|
||||||
|
0x7A, 0xF6, 0x21, 0x72, 0xFD, 0x41, 0x7A, 0xFD, 0xCD, 0x21, 0x73, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||||
|
0x6D, 0xFD, 0x21, 0x7A, 0xFD, 0xA0, 0x17, 0x82, 0x21, 0x82, 0xFD, 0x22, 0xC5, 0x69, 0xFD, 0xFA,
|
||||||
|
0x21, 0x7A, 0xFB, 0x21, 0x72, 0xFD, 0x22, 0x73, 0x65, 0xEC, 0xFD, 0x41, 0x6C, 0xD3, 0x22, 0x21,
|
||||||
|
0x61, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x6B, 0xE2, 0xC5, 0x21,
|
||||||
|
0x69, 0xFC, 0x21, 0x61, 0xFD, 0x22, 0x6E, 0x7A, 0xF3, 0xFD, 0xA0, 0x17, 0xA3, 0x21, 0x6C, 0xFD,
|
||||||
|
0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x21,
|
||||||
|
0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x5A, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||||
|
0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6D, 0x2E, 0x76, 0x78, 0x61, 0x69, 0x6F, 0x75,
|
||||||
|
0xF8, 0x15, 0xFD, 0x56, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04,
|
||||||
|
0xF8, 0x04, 0xFF, 0x5D, 0xF8, 0x04, 0xFF, 0x73, 0xF8, 0x04, 0xF8, 0x04, 0xFF, 0x80, 0xF8, 0x04,
|
||||||
|
0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x3A, 0xF5, 0xB4, 0xF5, 0xBC, 0xF5, 0xBC, 0xFF, 0x9D, 0xFF, 0xC1,
|
||||||
|
0xFF, 0xE0, 0xFF, 0xFD, 0xC2, 0x14, 0x82, 0x6E, 0x2E, 0xF5, 0xD5, 0xF5, 0x65, 0x41, 0x73, 0xEF,
|
||||||
|
0xFD, 0x21, 0x77, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x82, 0xFD, 0x41, 0x76, 0xEF,
|
||||||
|
0x40, 0x22, 0xC5, 0x72, 0xF9, 0xFC, 0x57, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6E, 0x2E, 0x76, 0x78, 0x61, 0xF7, 0xA4,
|
||||||
|
0xFC, 0xE5, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93,
|
||||||
|
0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xFF, 0xDE, 0xF7, 0x93,
|
||||||
|
0xF7, 0x93, 0xF7, 0xC9, 0xF5, 0x43, 0xF5, 0x4B, 0xF5, 0x4B, 0xFF, 0xFB, 0x41, 0x75, 0xFA, 0x78,
|
||||||
|
0xC2, 0x14, 0x82, 0x65, 0x2E, 0xFF, 0xFC, 0xF4, 0xF9, 0xA0, 0x14, 0x82, 0xC2, 0x14, 0x82, 0x7A,
|
||||||
|
0x2E, 0xFF, 0xFD, 0xF4, 0xED, 0x42, 0x6E, 0x2E, 0xF5, 0x54, 0xF4, 0xE4, 0x41, 0x6C, 0xF5, 0xB7,
|
||||||
|
0x21, 0x61, 0xFC, 0x21, 0x68, 0xFD, 0x41, 0x61, 0xF8, 0x12, 0x21, 0x70, 0xFC, 0x21, 0x7A, 0xFD,
|
||||||
|
0x21, 0x73, 0xFD, 0x44, 0x2E, 0x7A, 0x63, 0x65, 0xF4, 0xC6, 0xF4, 0xCE, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||||
|
0x41, 0x72, 0xF7, 0x82, 0x21, 0x65, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x42, 0x61, 0x68,
|
||||||
|
0xD2, 0xC6, 0xD2, 0xC3, 0x21, 0x63, 0xF9, 0x22, 0x6E, 0x73, 0xF3, 0xFD, 0x41, 0x69, 0xCD, 0x37,
|
||||||
|
0x21, 0x6E, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x82, 0xFD, 0x41, 0x7A, 0xD2, 0x7C,
|
||||||
|
0x21, 0x72, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
|
||||||
|
0x85, 0x99, 0xE1, 0x58, 0xE1, 0x58, 0x42, 0xC4, 0x69, 0xFF, 0xF9, 0xE1, 0x51, 0x21, 0x6E, 0xF9,
|
||||||
|
0xA0, 0x17, 0xD4, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||||
|
0x6D, 0xFD, 0x41, 0x64, 0xEE, 0xF8, 0x21, 0x6E, 0xFC, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6C, 0xF3,
|
||||||
|
0xFD, 0x21, 0x74, 0xFB, 0x41, 0x65, 0xD2, 0x3D, 0x21, 0x72, 0xFC, 0x42, 0x65, 0x75, 0xD4, 0xF0,
|
||||||
|
0xFF, 0xFD, 0x21, 0x69, 0xF9, 0x41, 0x69, 0xCD, 0xBE, 0x21, 0x6E, 0xFC, 0x21, 0x64, 0xFD, 0x21,
|
||||||
|
0x65, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x46, 0xC5, 0x64, 0x6D, 0x72, 0x6C, 0x77, 0xFF,
|
||||||
|
0x91, 0xFF, 0xA4, 0xFF, 0xB5, 0xFF, 0xD9, 0xFF, 0xEA, 0xFF, 0xFD, 0x58, 0xC4, 0xC5, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x73, 0x74, 0x7A, 0x6C, 0x70, 0x2E, 0x68, 0x6A, 0x72, 0x76,
|
||||||
|
0x77, 0x78, 0x61, 0x6F, 0xF6, 0x6F, 0xF7, 0xB3, 0xF6, 0x5E, 0xF6, 0x5E, 0xF6, 0x5E, 0xF6, 0x5E,
|
||||||
|
0xF6, 0x5E, 0xF6, 0x5E, 0xF6, 0x5E, 0xFF, 0x15, 0xFF, 0x21, 0xF6, 0x5E, 0xF6, 0x5E, 0xFF, 0x2A,
|
||||||
|
0xF6, 0x94, 0xF4, 0x0E, 0xF4, 0x16, 0xF4, 0x16, 0xFF, 0x48, 0xF4, 0x16, 0xF4, 0x16, 0xF4, 0x16,
|
||||||
|
0xFF, 0x6C, 0xFF, 0xED, 0xC2, 0x14, 0x82, 0x2E, 0x7A, 0xF3, 0xC5, 0xF3, 0xCD, 0x45, 0x82, 0x84,
|
||||||
|
0x9B, 0xBA, 0xBC, 0xF3, 0xBF, 0xF7, 0x71, 0xF7, 0x71, 0xF7, 0x71, 0xF7, 0x71, 0xD3, 0x02, 0x31,
|
||||||
|
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x77, 0x2E, 0xF7, 0x64, 0xFF, 0xF0, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7,
|
||||||
|
0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7,
|
||||||
|
0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF3, 0xAC, 0x41, 0x6E, 0xF6, 0x1C, 0x21, 0x65, 0xFC,
|
||||||
|
0x21, 0x67, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x7A, 0xD1, 0x77, 0x21, 0x63, 0xFC,
|
||||||
|
0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x18, 0x14, 0x21, 0x6B, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||||
|
0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x23, 0x65, 0x6B, 0x73, 0xE1, 0xEE, 0xFD, 0x57, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0x72,
|
||||||
|
0x2E, 0x76, 0x78, 0x6F, 0xF5, 0x9E, 0xFA, 0xDF, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D,
|
||||||
|
0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D,
|
||||||
|
0xFF, 0x78, 0xF5, 0x8D, 0xF5, 0x8D, 0xFF, 0x91, 0xF5, 0xC3, 0xF3, 0x3D, 0xF3, 0x45, 0xF3, 0x45,
|
||||||
|
0xFF, 0xF9, 0x45, 0x82, 0xBA, 0xBC, 0x84, 0x9B, 0xF9, 0xF1, 0xF5, 0x47, 0xF5, 0x47, 0xF2, 0xFF,
|
||||||
|
0xF2, 0xFF, 0x42, 0x2E, 0x7A, 0xF2, 0xE7, 0xF2, 0xEF, 0x46, 0x6B, 0x6E, 0x73, 0x2E, 0x72, 0x77,
|
||||||
|
0xF3, 0x50, 0xF3, 0x50, 0xF7, 0xA5, 0xF2, 0xE0, 0xFF, 0xF9, 0xF2, 0xE8, 0x41, 0x9B, 0xF6, 0x82,
|
||||||
|
0x42, 0x2E, 0x62, 0xF2, 0xC9, 0xF2, 0xD1, 0xA1, 0x14, 0x62, 0x7A, 0xF9, 0xC1, 0x14, 0x62, 0x6E,
|
||||||
|
0xF3, 0x2D, 0x41, 0x6D, 0xF9, 0xC8, 0x21, 0x6C, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21,
|
||||||
|
0x6B, 0xFD, 0xC3, 0x14, 0x62, 0x2E, 0x72, 0x6F, 0xF2, 0xA7, 0xF2, 0xAF, 0xFF, 0xFD, 0xA0, 0x18,
|
||||||
|
0x52, 0x21, 0x79, 0xFD, 0x21, 0x82, 0xFD, 0x21, 0xC5, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD,
|
||||||
|
0x41, 0x67, 0xF5, 0x15, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x87, 0xCA,
|
||||||
|
0xC6, 0x21, 0xC4, 0xFC, 0x21, 0x9B, 0xFD, 0x22, 0x7A, 0xC5, 0xF3, 0xFD, 0x41, 0x73, 0xF9, 0x7E,
|
||||||
|
0x21, 0x61, 0xFC, 0x21, 0x77, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xD1, 0x02, 0x31, 0xC4,
|
||||||
|
0xC5, 0x63, 0x66, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0x2E, 0x61, 0x65, 0x79,
|
||||||
|
0xF6, 0x15, 0xFF, 0x90, 0xFF, 0x9B, 0xF6, 0x12, 0xF2, 0x60, 0xFF, 0xA0, 0xF6, 0x12, 0xF2, 0x60,
|
||||||
|
0xF6, 0x12, 0xF6, 0x12, 0xFF, 0xB6, 0xF6, 0x12, 0xF6, 0x12, 0xF2, 0x5D, 0xFF, 0xD1, 0xFF, 0xEB,
|
||||||
|
0xFF, 0xFD, 0x42, 0x2E, 0x68, 0xF2, 0x27, 0xF2, 0x2F, 0x41, 0x7A, 0xF2, 0x28, 0x41, 0x72, 0xFC,
|
||||||
|
0x18, 0x21, 0x65, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD,
|
||||||
|
0x43, 0x2E, 0x72, 0x6F, 0xF2, 0x09, 0xFF, 0xE9, 0xFF, 0xFD, 0x42, 0x2E, 0x72, 0xF1, 0xFF, 0xF2,
|
||||||
|
0xD9, 0x41, 0x65, 0xD0, 0x0F, 0x21, 0x6C, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x7A, 0xCF, 0xE6, 0x21,
|
||||||
|
0x69, 0xFC, 0x21, 0x77, 0xFD, 0x41, 0x74, 0xCF, 0xFB, 0x21, 0x70, 0xFC, 0x21, 0x6F, 0xFD, 0x41,
|
||||||
|
0x6D, 0xCE, 0x7F, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x6E, 0x73, 0xF3, 0xFD, 0x59, 0xC5,
|
||||||
|
0x62, 0x64, 0x66, 0x67, 0x73, 0x6E, 0x74, 0x7A, 0x2E, 0xC4, 0x63, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x70, 0x72, 0x76, 0x77, 0x78, 0x65, 0x6F, 0x79, 0xFE, 0xD4, 0xF4, 0x1B, 0xF4, 0x1B, 0xF4, 0x1B,
|
||||||
|
0xF4, 0x1B, 0xF4, 0x1B, 0xF4, 0x14, 0xFE, 0xEB, 0xFF, 0x6E, 0xF1, 0xCB, 0xF8, 0x14, 0xFF, 0xA4,
|
||||||
|
0xF1, 0xD3, 0xF1, 0xD3, 0xFF, 0xC2, 0xF1, 0xD3, 0xFF, 0xCC, 0xF1, 0xD3, 0xF1, 0xD3, 0xF1, 0xD3,
|
||||||
|
0xF1, 0xD3, 0xF1, 0xD3, 0xFF, 0xDA, 0xFF, 0xE4, 0xFF, 0xFB, 0x43, 0x6B, 0x7A, 0x2E, 0xF1, 0xEF,
|
||||||
|
0xFC, 0x9B, 0xF1, 0x7F, 0xA0, 0x18, 0x73, 0x21, 0x74, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x79, 0xFD,
|
||||||
|
0x41, 0x74, 0xE0, 0xAB, 0x21, 0x75, 0xFC, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD,
|
||||||
|
0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x69, 0xCF, 0x45,
|
||||||
|
0x21, 0x6E, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x67, 0xFD, 0x59, 0xC4, 0xC5, 0x62,
|
||||||
|
0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x7A, 0x6C, 0x72, 0x74, 0x2E, 0x68, 0x6A,
|
||||||
|
0x76, 0x77, 0x78, 0x6F, 0x75, 0x79, 0xF3, 0x9E, 0xF4, 0xE2, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D,
|
||||||
|
0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D,
|
||||||
|
0xFC, 0x59, 0xFF, 0xBE, 0xF3, 0xC3, 0xF1, 0x3D, 0xF1, 0x45, 0xF1, 0x45, 0xF1, 0x45, 0xF1, 0x45,
|
||||||
|
0xF1, 0x45, 0xFF, 0xD1, 0xFF, 0xED, 0xFF, 0xFD, 0x41, 0x64, 0xF6, 0x6C, 0x41, 0x73, 0xF6, 0x68,
|
||||||
|
0xC3, 0x14, 0x82, 0x61, 0x6F, 0x2E, 0xFF, 0xF8, 0xFF, 0xFC, 0xF0, 0xE9, 0x45, 0x82, 0x84, 0x9B,
|
||||||
|
0xBA, 0xBC, 0xFF, 0xF4, 0xF3, 0x2D, 0xF3, 0x2D, 0xF3, 0x2D, 0xF3, 0x2D, 0x21, 0x61, 0xE0, 0xC2,
|
||||||
|
0x14, 0x82, 0x7A, 0x2E, 0xFF, 0xFD, 0xF0, 0xCA, 0xA0, 0x18, 0xA3, 0x21, 0x64, 0xFD, 0x21, 0x6E,
|
||||||
|
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6B, 0xFD, 0x42, 0x66, 0x6D, 0xEB, 0x53, 0xEB, 0x53, 0x21, 0x74,
|
||||||
|
0xF9, 0x22, 0x65, 0x73, 0xF3, 0xFD, 0x57, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B,
|
||||||
|
0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x77, 0x2E, 0x68, 0x76, 0x78, 0x65, 0xF3, 0x04,
|
||||||
|
0xFF, 0xC6, 0xF2, 0xF3, 0xFF, 0xD9, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3,
|
||||||
|
0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3,
|
||||||
|
0xF3, 0x29, 0xF0, 0xA3, 0xF0, 0xAB, 0xF0, 0xAB, 0xF0, 0xAB, 0xFF, 0xFB, 0x45, 0x9B, 0x82, 0x84,
|
||||||
|
0xBA, 0xBC, 0xF2, 0xAD, 0xF0, 0x65, 0xF0, 0x65, 0xF0, 0x65, 0xF0, 0x65, 0xC4, 0x14, 0x82, 0x6B,
|
||||||
|
0x6E, 0x2E, 0x72, 0xF0, 0xBD, 0xF0, 0xBD, 0xF0, 0x4D, 0xFD, 0x66, 0x41, 0x70, 0xEA, 0x32, 0x21,
|
||||||
|
0x70, 0xFC, 0x57, 0xC4, 0xC5, 0x63, 0x64, 0x66, 0x6B, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0x62, 0x67,
|
||||||
|
0x68, 0x6A, 0x6C, 0x6D, 0x6E, 0x72, 0x76, 0x77, 0x78, 0x65, 0xF2, 0x98, 0xFF, 0xDA, 0xF2, 0x87,
|
||||||
|
0xFF, 0xEA, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0xBD, 0xF0, 0x37,
|
||||||
|
0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F,
|
||||||
|
0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xFF, 0xFD, 0xA0, 0x15, 0xB3, 0x21, 0x73, 0xFD, 0x21, 0x6B,
|
||||||
|
0xFD, 0x21, 0x6C, 0xFD, 0x57, 0x2E, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||||
|
0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0x6F, 0xEF, 0xE5, 0xF6, 0x2E,
|
||||||
|
0xF6, 0x32, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED,
|
||||||
|
0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED,
|
||||||
|
0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xFF, 0xFD, 0x56, 0x2E, 0xC4, 0xC5, 0x62, 0x63,
|
||||||
|
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||||
|
0x7A, 0xEF, 0x9F, 0xF5, 0xE8, 0xF5, 0xEC, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF,
|
||||||
|
0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF,
|
||||||
|
0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xA0, 0x00, 0xD1,
|
||||||
|
0xC6, 0x02, 0x61, 0x75, 0x79, 0x61, 0x65, 0x69, 0x6F, 0xFF, 0xFD, 0xFF, 0xFD, 0xF0, 0x33, 0xF0,
|
||||||
|
0x33, 0xF0, 0x33, 0xF0, 0x33, 0xA0, 0x04, 0xF2, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x7A,
|
||||||
|
0xFD, 0xA0, 0x04, 0xE2, 0x21, 0x7A, 0xFD, 0xA1, 0x00, 0xD1, 0x73, 0xFD, 0xC7, 0x02, 0x61, 0x72,
|
||||||
|
0x75, 0x79, 0x61, 0x65, 0x69, 0x6F, 0xFF, 0xF2, 0xFF, 0xFB, 0xFF, 0xD1, 0xF0, 0x07, 0xF0, 0x07,
|
||||||
|
0xF0, 0x07, 0xF0, 0x07, 0x22, 0x85, 0x99, 0xB9, 0xB9, 0x21, 0xB3, 0xB4, 0x41, 0x6B, 0xE9, 0xB1,
|
||||||
|
0x21, 0x63, 0xFC, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x73, 0xFD, 0x41,
|
||||||
|
0x63, 0xE9, 0x9E, 0x21, 0x75, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x22, 0x6E, 0x73, 0xF0,
|
||||||
|
0xFD, 0xC9, 0x02, 0x61, 0xC4, 0xC3, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6E, 0xFF, 0xD3, 0xFF,
|
||||||
|
0xD8, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0xFB, 0x41,
|
||||||
|
0x72, 0xDB, 0xBC, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||||
|
0x68, 0xFD, 0x41, 0x65, 0xFC, 0xD3, 0x21, 0x73, 0xFC, 0x21, 0x66, 0xFD, 0xC8, 0x02, 0x61, 0x79,
|
||||||
|
0x61, 0x63, 0x65, 0x66, 0x69, 0x6F, 0x75, 0xFF, 0x51, 0xEF, 0x87, 0xFF, 0xF3, 0xEF, 0x87, 0xFF,
|
||||||
|
0xFD, 0xEF, 0x87, 0xEF, 0x87, 0xEF, 0x87, 0xC6, 0x02, 0x61, 0x79, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||||
|
0xFF, 0x36, 0xEF, 0x6C, 0xEF, 0x6C, 0xEF, 0x6C, 0xEF, 0x6C, 0xEF, 0x6C, 0xC5, 0x02, 0x61, 0x61,
|
||||||
|
0x65, 0x69, 0x6F, 0x75, 0xEF, 0x57, 0xEF, 0x57, 0xEF, 0x57, 0xEF, 0x57, 0xEF, 0x57, 0x5D, 0x2E,
|
||||||
|
0xC4, 0xC3, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72,
|
||||||
|
0x73, 0x74, 0x77, 0x7A, 0x76, 0x78, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xEE, 0x19, 0xEE, 0xB6,
|
||||||
|
0xEE, 0xCC, 0xF0, 0xA4, 0xF1, 0xC1, 0xF2, 0xE7, 0xF4, 0x6B, 0xF5, 0x1C, 0xF5, 0xC4, 0xF6, 0x3C,
|
||||||
|
0xF6, 0xA5, 0xF7, 0x4A, 0xF7, 0xC5, 0xF8, 0xB7, 0xF9, 0x28, 0xFA, 0x5D, 0xFB, 0x2E, 0xFC, 0xA0,
|
||||||
|
0xFD, 0x2E, 0xFD, 0xC8, 0xFE, 0x34, 0xFE, 0x86, 0xFE, 0xCC, 0xFF, 0x12, 0xFF, 0x3E, 0xFF, 0x83,
|
||||||
|
0xFF, 0xBE, 0xFF, 0xD9, 0xFF, 0xEE,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns pl_patterns = {
|
||||||
|
0x3C4Eu,
|
||||||
|
pl_trie_data,
|
||||||
|
sizeof(pl_trie_data),
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include <climits>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Epub/FootnoteEntry.h"
|
||||||
|
#include "Epub/ParsedText.h"
|
||||||
|
#include "Epub/blocks/ImageBlock.h"
|
||||||
|
#include "Epub/blocks/TextBlock.h"
|
||||||
|
#include "Epub/css/CssParser.h"
|
||||||
|
#include "Epub/css/CssStyle.h"
|
||||||
|
|
||||||
|
class Page;
|
||||||
|
class GfxRenderer;
|
||||||
|
class Epub;
|
||||||
|
|
||||||
|
#define MAX_WORD_SIZE 200
|
||||||
|
|
||||||
|
class ChapterHtmlSlimParser {
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
|
const std::string& filepath;
|
||||||
|
GfxRenderer& renderer;
|
||||||
|
std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)> completePageFn;
|
||||||
|
std::function<void()> popupFn; // Popup callback
|
||||||
|
bool imagePopupFired = false; // popupFn fired for the first image probe (single-shot)
|
||||||
|
int depth = 0;
|
||||||
|
int skipUntilDepth = INT_MAX;
|
||||||
|
int boldUntilDepth = INT_MAX;
|
||||||
|
int italicUntilDepth = 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;
|
||||||
|
int fontId;
|
||||||
|
float lineCompression;
|
||||||
|
bool extraParagraphSpacing;
|
||||||
|
uint8_t paragraphAlignment;
|
||||||
|
uint16_t viewportWidth;
|
||||||
|
uint16_t viewportHeight;
|
||||||
|
bool hyphenationEnabled;
|
||||||
|
bool focusReadingEnabled;
|
||||||
|
const CssParser* cssParser;
|
||||||
|
bool embeddedStyle;
|
||||||
|
uint8_t imageRendering;
|
||||||
|
std::string contentBase;
|
||||||
|
std::string imageBasePath;
|
||||||
|
int imageCounter = 0;
|
||||||
|
|
||||||
|
// Style tracking (replaces depth-based approach)
|
||||||
|
struct StyleStackEntry {
|
||||||
|
int depth = 0;
|
||||||
|
bool hasBold = false, bold = false;
|
||||||
|
bool hasItalic = false, italic = false;
|
||||||
|
bool hasTextDecoration = false;
|
||||||
|
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||||
|
bool hasDirection = false;
|
||||||
|
CssTextDirection direction = CssTextDirection::Ltr;
|
||||||
|
bool hasSup = false, sup = false;
|
||||||
|
bool hasSub = false, sub = false;
|
||||||
|
};
|
||||||
|
std::vector<StyleStackEntry> inlineStyleStack;
|
||||||
|
std::vector<BlockStyle> blockStyleStack; // accumulated block styles from open ancestor elements
|
||||||
|
CssStyle currentCssStyle;
|
||||||
|
bool effectiveBold = false;
|
||||||
|
bool effectiveItalic = false;
|
||||||
|
CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
|
||||||
|
bool effectiveDirectionDefined = false;
|
||||||
|
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
|
||||||
|
bool effectiveSup = false;
|
||||||
|
bool effectiveSub = false;
|
||||||
|
int tableDepth = 0;
|
||||||
|
int tableRowIndex = 0;
|
||||||
|
int tableColIndex = 0;
|
||||||
|
bool listItemBulletOnly = false; // true when currentTextBlock has only the <li> bullet
|
||||||
|
|
||||||
|
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
||||||
|
int completedPageCount = 0;
|
||||||
|
std::vector<std::pair<std::string, uint16_t>> anchorData;
|
||||||
|
std::string pendingAnchorId; // deferred until after previous text block is flushed
|
||||||
|
std::vector<std::string> tocAnchors; // the list of anchors that are TOC chapter boundaries
|
||||||
|
uint16_t xpathParagraphIndex = 0;
|
||||||
|
uint16_t xpathListItemIndex = 0;
|
||||||
|
|
||||||
|
// Footnote link tracking
|
||||||
|
bool insideFootnoteLink = false;
|
||||||
|
int footnoteLinkDepth = -1;
|
||||||
|
FootnoteEntry currentFootnote = {};
|
||||||
|
int currentFootnoteLinkTextLen = 0;
|
||||||
|
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
||||||
|
int wordsExtractedInBlock = 0;
|
||||||
|
|
||||||
|
// Resumable parse state. The one-shot parseAndBuildPages() drives these
|
||||||
|
// internally; the incremental section builder drives them across render ticks
|
||||||
|
// so a large single chapter can yield between pages instead of blocking the UI
|
||||||
|
// until the whole thing is laid out. parseFile_ and the expat parser stay alive
|
||||||
|
// for the lifetime of the parse so it can be paused and resumed at buffer
|
||||||
|
// boundaries.
|
||||||
|
XML_Parser xmlParser_ = nullptr;
|
||||||
|
HalFile parseFile_;
|
||||||
|
uint32_t parseStartTime_ = 0;
|
||||||
|
|
||||||
|
void updateEffectiveInlineStyle();
|
||||||
|
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||||
|
void flushPendingAnchor();
|
||||||
|
void flushPartWordBuffer();
|
||||||
|
void makePages();
|
||||||
|
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
|
||||||
|
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||||
|
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||||
|
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
|
||||||
|
void emitHorizontalRule(const BlockStyle& blockStyle);
|
||||||
|
// XML callbacks
|
||||||
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
|
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||||
|
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
|
||||||
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
|
||||||
|
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||||
|
const bool focusReadingEnabled,
|
||||||
|
const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn,
|
||||||
|
const bool embeddedStyle, const std::string& contentBase,
|
||||||
|
const std::string& imageBasePath, const uint8_t imageRendering = 0,
|
||||||
|
std::vector<std::string> tocAnchors = {},
|
||||||
|
const std::function<void()>& popupFn = nullptr, const CssParser* cssParser = nullptr)
|
||||||
|
|
||||||
|
: epub(epub),
|
||||||
|
filepath(filepath),
|
||||||
|
renderer(renderer),
|
||||||
|
fontId(fontId),
|
||||||
|
lineCompression(lineCompression),
|
||||||
|
extraParagraphSpacing(extraParagraphSpacing),
|
||||||
|
paragraphAlignment(paragraphAlignment),
|
||||||
|
viewportWidth(viewportWidth),
|
||||||
|
viewportHeight(viewportHeight),
|
||||||
|
hyphenationEnabled(hyphenationEnabled),
|
||||||
|
focusReadingEnabled(focusReadingEnabled),
|
||||||
|
completePageFn(completePageFn),
|
||||||
|
popupFn(popupFn),
|
||||||
|
cssParser(cssParser),
|
||||||
|
embeddedStyle(embeddedStyle),
|
||||||
|
imageRendering(imageRendering),
|
||||||
|
contentBase(contentBase),
|
||||||
|
imageBasePath(imageBasePath),
|
||||||
|
tocAnchors(std::move(tocAnchors)) {}
|
||||||
|
|
||||||
|
~ChapterHtmlSlimParser();
|
||||||
|
|
||||||
|
// One-shot parse: builds every page before returning (begin + step* + finish).
|
||||||
|
bool parseAndBuildPages();
|
||||||
|
|
||||||
|
// Resumable parse, for the incremental section builder. Drive as:
|
||||||
|
// if (!beginParse()) fail;
|
||||||
|
// loop: switch (parseStep()) { More: keep going / yield; Done: finishParse(); Error: abortParse(); }
|
||||||
|
// Pages are emitted via completePageFn as they complete during parseStep(), so
|
||||||
|
// the caller can stop once enough pages are built and resume on a later tick.
|
||||||
|
enum class ParseStatus { More, Done, Error };
|
||||||
|
bool beginParse();
|
||||||
|
ParseStatus parseStep();
|
||||||
|
bool finishParse(); // flush the trailing page and tear down; returns true
|
||||||
|
void abortParse(); // tear down without flushing (error / abandon)
|
||||||
|
|
||||||
|
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||||
|
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
|
||||||
|
|
||||||
|
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
|
||||||
|
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
|
||||||
|
// between beginParse() and finishParse()/abortParse().
|
||||||
|
size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : 0; }
|
||||||
|
size_t parseTotalBytes() { return parseFile_ ? parseFile_.size() : 0; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "ContainerParser.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <XmlParserUtils.h>
|
||||||
|
|
||||||
|
bool ContainerParser::setup() {
|
||||||
|
parser = XML_ParserCreate(nullptr);
|
||||||
|
if (!parser) {
|
||||||
|
LOG_ERR("CTR", "Couldn't allocate memory for parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XML_SetUserData(parser, this);
|
||||||
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerParser::~ContainerParser() { destroyXmlParser(parser); }
|
||||||
|
|
||||||
|
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }
|
||||||
|
|
||||||
|
size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
||||||
|
if (!parser) return 0;
|
||||||
|
|
||||||
|
const uint8_t* currentBufferPos = buffer;
|
||||||
|
auto remainingInBuffer = size;
|
||||||
|
|
||||||
|
while (remainingInBuffer > 0) {
|
||||||
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
|
if (!buf) {
|
||||||
|
LOG_DBG("CTR", "Couldn't allocate buffer");
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||||
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
|
LOG_ERR("CTR", "Parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBufferPos += toRead;
|
||||||
|
remainingInBuffer -= toRead;
|
||||||
|
remainingSize -= toRead;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL ContainerParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
|
auto* self = static_cast<ContainerParser*>(userData);
|
||||||
|
|
||||||
|
// Simple state tracking to ensure we are looking at the valid schema structure
|
||||||
|
if (self->state == START && strcmp(name, "container") == 0) {
|
||||||
|
self->state = IN_CONTAINER;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_CONTAINER && strcmp(name, "rootfiles") == 0) {
|
||||||
|
self->state = IN_ROOTFILES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_ROOTFILES && strcmp(name, "rootfile") == 0) {
|
||||||
|
const char* mediaType = nullptr;
|
||||||
|
const char* path = nullptr;
|
||||||
|
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "media-type") == 0) {
|
||||||
|
mediaType = atts[i + 1];
|
||||||
|
} else if (strcmp(atts[i], "full-path") == 0) {
|
||||||
|
path = atts[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is the standard OEBPS package
|
||||||
|
if (mediaType && path && strcmp(mediaType, "application/oebps-package+xml") == 0) {
|
||||||
|
self->fullPath = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL ContainerParser::endElement(void* userData, const XML_Char* name) {
|
||||||
|
auto* self = static_cast<ContainerParser*>(userData);
|
||||||
|
|
||||||
|
if (self->state == IN_ROOTFILES && strcmp(name, "rootfiles") == 0) {
|
||||||
|
self->state = IN_CONTAINER;
|
||||||
|
} else if (self->state == IN_CONTAINER && strcmp(name, "container") == 0) {
|
||||||
|
self->state = START;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Print.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "expat.h"
|
||||||
|
|
||||||
|
class ContainerParser final : public Print {
|
||||||
|
enum ParserState {
|
||||||
|
START,
|
||||||
|
IN_CONTAINER,
|
||||||
|
IN_ROOTFILES,
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t remainingSize;
|
||||||
|
XML_Parser parser = nullptr;
|
||||||
|
ParserState state = START;
|
||||||
|
|
||||||
|
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
|
static void endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string fullPath;
|
||||||
|
|
||||||
|
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
|
||||||
|
~ContainerParser() override;
|
||||||
|
|
||||||
|
bool setup();
|
||||||
|
|
||||||
|
size_t write(uint8_t) override;
|
||||||
|
size_t write(const uint8_t* buffer, size_t size) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
#include "ContentOpfParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
#include <XmlParserUtils.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||||
|
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||||
|
constexpr char MEDIA_TYPE_IMAGE_PREFIX[] = "image/";
|
||||||
|
constexpr char itemCacheFile[] = "/.items.bin";
|
||||||
|
|
||||||
|
bool startsWithImageMediaType(const std::string& mediaType) {
|
||||||
|
constexpr size_t prefixLen = sizeof(MEDIA_TYPE_IMAGE_PREFIX) - 1;
|
||||||
|
if (mediaType.size() < prefixLen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < prefixLen; ++i) {
|
||||||
|
const char c = static_cast<char>(std::tolower(static_cast<unsigned char>(mediaType[i])));
|
||||||
|
if (c != MEDIA_TYPE_IMAGE_PREFIX[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ContentOpfParser::setup() {
|
||||||
|
parser = XML_ParserCreate(nullptr);
|
||||||
|
if (!parser) {
|
||||||
|
LOG_DBG("COF", "Couldn't allocate memory for parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XML_SetUserData(parser, this);
|
||||||
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
|
XML_SetCharacterDataHandler(parser, characterData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentOpfParser::~ContentOpfParser() {
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
if (tempItemStore) {
|
||||||
|
tempItemStore.close();
|
||||||
|
}
|
||||||
|
const auto itemCachePath = cachePath + itemCacheFile;
|
||||||
|
if (Storage.exists(itemCachePath.c_str())) {
|
||||||
|
Storage.remove(itemCachePath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||||
|
|
||||||
|
size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
||||||
|
if (!parser) return 0;
|
||||||
|
|
||||||
|
const uint8_t* currentBufferPos = buffer;
|
||||||
|
auto remainingInBuffer = size;
|
||||||
|
|
||||||
|
while (remainingInBuffer > 0) {
|
||||||
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
|
|
||||||
|
if (!buf) {
|
||||||
|
LOG_ERR("COF", "Couldn't allocate memory for buffer");
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||||
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
|
LOG_DBG("COF", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBufferPos += toRead;
|
||||||
|
remainingInBuffer -= toRead;
|
||||||
|
remainingSize -= toRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
|
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||||
|
(void)atts;
|
||||||
|
|
||||||
|
if (self->state == START && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
|
||||||
|
self->state = IN_PACKAGE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_PACKAGE && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||||
|
self->state = IN_METADATA;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) {
|
||||||
|
// Only capture the first dc:title element; subsequent ones are subtitles
|
||||||
|
if (self->title.empty()) {
|
||||||
|
self->state = IN_BOOK_TITLE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_METADATA && strcmp(name, "dc:creator") == 0) {
|
||||||
|
self->state = IN_BOOK_AUTHOR;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_METADATA && strcmp(name, "dc:language") == 0) {
|
||||||
|
self->state = IN_BOOK_LANGUAGE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
|
self->state = IN_MANIFEST;
|
||||||
|
if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
LOG_ERR("COF", "Couldn't open temp items file for writing. This is probably going to be a fatal error.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||||
|
self->state = IN_SPINE;
|
||||||
|
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the (unconditionally-built) item index so every idref lookup uses binary
|
||||||
|
// search. Without this, small/medium manifests fell back to an O(spine × manifest)
|
||||||
|
// linear rescan of .items.bin per itemref (up to ~200ms/item at large scale).
|
||||||
|
if (!self->itemIndex.empty()) {
|
||||||
|
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;
|
||||||
|
LOG_DBG("COF", "Using fast index for %zu manifest items", self->itemIndex.size());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
|
self->state = IN_GUIDE;
|
||||||
|
// TODO Remove print
|
||||||
|
LOG_DBG("COF", "Entering guide state.");
|
||||||
|
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
|
||||||
|
bool isCover = false;
|
||||||
|
std::string coverItemId;
|
||||||
|
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "name") == 0 && strcmp(atts[i + 1], "cover") == 0) {
|
||||||
|
isCover = true;
|
||||||
|
} else if (strcmp(atts[i], "content") == 0) {
|
||||||
|
coverItemId = atts[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCover) {
|
||||||
|
self->coverItemId = coverItemId;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
|
||||||
|
std::string itemId;
|
||||||
|
std::string href;
|
||||||
|
std::string mediaType;
|
||||||
|
std::string properties;
|
||||||
|
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "id") == 0) {
|
||||||
|
itemId = atts[i + 1];
|
||||||
|
} else if (strcmp(atts[i], "href") == 0) {
|
||||||
|
href = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(self->baseContentPath + atts[i + 1]));
|
||||||
|
} else if (strcmp(atts[i], "media-type") == 0) {
|
||||||
|
mediaType = atts[i + 1];
|
||||||
|
} else if (strcmp(atts[i], "properties") == 0) {
|
||||||
|
properties = atts[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
if (itemId == self->coverItemId) {
|
||||||
|
// Some EPUBs set meta name="cover" to an XHTML wrapper item.
|
||||||
|
// Only treat it as a cover image when the manifest media-type is image/*.
|
||||||
|
if (startsWithImageMediaType(mediaType)) {
|
||||||
|
self->coverItemHref = href;
|
||||||
|
} else {
|
||||||
|
LOG_DBG("COF", "Ignoring meta cover item '%s' with non-image media type: %s", itemId.c_str(),
|
||||||
|
mediaType.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType == MEDIA_TYPE_NCX) {
|
||||||
|
if (self->tocNcxPath.empty()) {
|
||||||
|
self->tocNcxPath = href;
|
||||||
|
} else {
|
||||||
|
LOG_DBG("COF", "Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s", href.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect CSS files
|
||||||
|
if (mediaType == MEDIA_TYPE_CSS) {
|
||||||
|
self->cssFiles.push_back(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EPUB 3: Check for nav document (properties contains "nav")
|
||||||
|
if (!properties.empty() && self->tocNavPath.empty()) {
|
||||||
|
// Properties is space-separated, check if "nav" is present as a word
|
||||||
|
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
||||||
|
self->tocNavPath = href;
|
||||||
|
LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EPUB 3: Check for cover image (properties contains "cover-image")
|
||||||
|
if (!properties.empty() && self->coverItemHref.empty()) {
|
||||||
|
if (properties == "cover-image" || properties.find("cover-image ") == 0 ||
|
||||||
|
properties.find(" cover-image") != std::string::npos) {
|
||||||
|
self->coverItemHref = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This relies on spine appearing after item manifest (which is pretty safe as it's part of the EPUB spec)
|
||||||
|
// Only run the spine parsing if there's a cache to add it to
|
||||||
|
if (self->cache) {
|
||||||
|
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "idref") == 0) {
|
||||||
|
const std::string idref = atts[i + 1];
|
||||||
|
std::string href;
|
||||||
|
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 {
|
||||||
|
// Fallback linear scan, only reached when the index is empty (no manifest
|
||||||
|
// items). The fast binary-search path above is used for all real manifests.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// parse the guide
|
||||||
|
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
||||||
|
std::string type;
|
||||||
|
std::string guideHref;
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "type") == 0) {
|
||||||
|
type = atts[i + 1];
|
||||||
|
} else if (strcmp(atts[i], "href") == 0) {
|
||||||
|
guideHref = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(self->baseContentPath + atts[i + 1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!guideHref.empty()) {
|
||||||
|
if (type == "text" || (type == "start" && !self->textReferenceHref.empty())) {
|
||||||
|
LOG_DBG("COF", "Found %s reference in guide: %s", type.c_str(), guideHref.c_str());
|
||||||
|
self->textReferenceHref = guideHref;
|
||||||
|
} else if ((type == "cover" || type == "cover-page") && self->guideCoverPageHref.empty()) {
|
||||||
|
LOG_DBG("COF", "Found cover reference in guide: %s", guideHref.c_str());
|
||||||
|
self->guideCoverPageHref = guideHref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||||
|
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_TITLE) {
|
||||||
|
self->title.append(s, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_AUTHOR) {
|
||||||
|
if (!self->author.empty()) {
|
||||||
|
self->author.append(", "); // Add separator for multiple authors
|
||||||
|
}
|
||||||
|
self->author.append(s, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_LANGUAGE) {
|
||||||
|
self->language.append(s, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
|
||||||
|
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||||
|
(void)name;
|
||||||
|
|
||||||
|
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||||
|
self->state = IN_PACKAGE;
|
||||||
|
self->tempItemStore.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_GUIDE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
|
self->state = IN_PACKAGE;
|
||||||
|
self->tempItemStore.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
|
self->state = IN_PACKAGE;
|
||||||
|
self->tempItemStore.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_TITLE && strcmp(name, "dc:title") == 0) {
|
||||||
|
self->state = IN_METADATA;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_AUTHOR && strcmp(name, "dc:creator") == 0) {
|
||||||
|
self->state = IN_METADATA;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_LANGUAGE && strcmp(name, "dc:language") == 0) {
|
||||||
|
self->state = IN_METADATA;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||||
|
self->state = IN_PACKAGE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_PACKAGE && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
|
||||||
|
self->state = START;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Print.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <deque>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Epub.h"
|
||||||
|
#include "expat.h"
|
||||||
|
|
||||||
|
class BookMetadataCache;
|
||||||
|
|
||||||
|
class ContentOpfParser final : public Print {
|
||||||
|
enum ParserState {
|
||||||
|
START,
|
||||||
|
IN_PACKAGE,
|
||||||
|
IN_METADATA,
|
||||||
|
IN_BOOK_TITLE,
|
||||||
|
IN_BOOK_AUTHOR,
|
||||||
|
IN_BOOK_LANGUAGE,
|
||||||
|
IN_MANIFEST,
|
||||||
|
IN_SPINE,
|
||||||
|
IN_GUIDE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string& cachePath;
|
||||||
|
const std::string& baseContentPath;
|
||||||
|
size_t remainingSize;
|
||||||
|
XML_Parser parser = nullptr;
|
||||||
|
ParserState state = START;
|
||||||
|
BookMetadataCache* cache;
|
||||||
|
HalFile tempItemStore;
|
||||||
|
std::string coverItemId;
|
||||||
|
|
||||||
|
// Index for fast idref→href lookup (binary search over .items.bin)
|
||||||
|
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::deque<ItemIndexEntry> itemIndex;
|
||||||
|
bool useItemIndex = false;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
std::string language;
|
||||||
|
std::string tocNcxPath;
|
||||||
|
std::string tocNavPath; // EPUB 3 nav document path
|
||||||
|
std::string coverItemHref;
|
||||||
|
std::string guideCoverPageHref; // Guide reference with type="cover" or "cover-page" (points to XHTML wrapper)
|
||||||
|
std::string textReferenceHref;
|
||||||
|
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
||||||
|
|
||||||
|
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||||
|
BookMetadataCache* cache)
|
||||||
|
: cachePath(cachePath), baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
|
~ContentOpfParser() override;
|
||||||
|
|
||||||
|
bool setup();
|
||||||
|
|
||||||
|
size_t write(uint8_t) override;
|
||||||
|
size_t write(const uint8_t* buffer, size_t size) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#include "TocNavParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <XmlParserUtils.h>
|
||||||
|
|
||||||
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
|
bool TocNavParser::setup() {
|
||||||
|
parser = XML_ParserCreate(nullptr);
|
||||||
|
if (!parser) {
|
||||||
|
LOG_DBG("NAV", "Couldn't allocate memory for parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XML_SetUserData(parser, this);
|
||||||
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
|
XML_SetCharacterDataHandler(parser, characterData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TocNavParser::~TocNavParser() { destroyXmlParser(parser); }
|
||||||
|
|
||||||
|
size_t TocNavParser::write(const uint8_t data) { return write(&data, 1); }
|
||||||
|
|
||||||
|
size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
||||||
|
if (!parser) return 0;
|
||||||
|
|
||||||
|
const uint8_t* currentBufferPos = buffer;
|
||||||
|
auto remainingInBuffer = size;
|
||||||
|
|
||||||
|
while (remainingInBuffer > 0) {
|
||||||
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
|
if (!buf) {
|
||||||
|
LOG_DBG("NAV", "Couldn't allocate memory for buffer");
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||||
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
|
LOG_DBG("NAV", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBufferPos += toRead;
|
||||||
|
remainingInBuffer -= toRead;
|
||||||
|
remainingSize -= toRead;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
|
auto* self = static_cast<TocNavParser*>(userData);
|
||||||
|
|
||||||
|
// Track HTML structure loosely - we mainly care about finding <nav epub:type="toc">
|
||||||
|
if (strcmp(name, "html") == 0) {
|
||||||
|
self->state = IN_HTML;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_HTML && strcmp(name, "body") == 0) {
|
||||||
|
self->state = IN_BODY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for <nav epub:type="toc"> anywhere in body (or nested elements)
|
||||||
|
if (self->state >= IN_BODY && strcmp(name, "nav") == 0) {
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
||||||
|
self->state = IN_NAV_TOC;
|
||||||
|
LOG_DBG("NAV", "Found nav toc element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process ol/li/a if we're inside the toc nav
|
||||||
|
if (self->state < IN_NAV_TOC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, "ol") == 0) {
|
||||||
|
self->olDepth++;
|
||||||
|
self->state = IN_OL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_OL && strcmp(name, "li") == 0) {
|
||||||
|
self->state = IN_LI;
|
||||||
|
self->currentLabel.clear();
|
||||||
|
self->currentHref.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_LI && strcmp(name, "a") == 0) {
|
||||||
|
self->state = IN_ANCHOR;
|
||||||
|
// Get href attribute
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "href") == 0) {
|
||||||
|
self->currentHref = atts[i + 1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL TocNavParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||||
|
auto* self = static_cast<TocNavParser*>(userData);
|
||||||
|
|
||||||
|
// Only collect text when inside an anchor within the TOC nav
|
||||||
|
if (self->state == IN_ANCHOR) {
|
||||||
|
self->currentLabel.append(s, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
|
||||||
|
auto* self = static_cast<TocNavParser*>(userData);
|
||||||
|
|
||||||
|
if (strcmp(name, "a") == 0 && self->state == IN_ANCHOR) {
|
||||||
|
// Create TOC entry when closing anchor tag (we have all data now)
|
||||||
|
if (!self->currentLabel.empty() && !self->currentHref.empty()) {
|
||||||
|
const std::string rawTarget = self->baseContentPath + self->currentHref;
|
||||||
|
const size_t pos = rawTarget.find('#');
|
||||||
|
const std::string rawPath = pos == std::string::npos ? rawTarget : rawTarget.substr(0, pos);
|
||||||
|
std::string href = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(rawPath));
|
||||||
|
std::string anchor;
|
||||||
|
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
anchor = FsHelpers::decodeUriEscapes(rawTarget.substr(pos + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->cache) {
|
||||||
|
// olDepth gives us the nesting level (1-based from the outer ol)
|
||||||
|
self->cache->createTocEntry(self->currentLabel, href, anchor, self->olDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
self->currentLabel.clear();
|
||||||
|
self->currentHref.clear();
|
||||||
|
}
|
||||||
|
self->state = IN_LI;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, "li") == 0 && (self->state == IN_LI || self->state == IN_OL)) {
|
||||||
|
self->state = IN_OL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, "ol") == 0 && self->state >= IN_NAV_TOC) {
|
||||||
|
self->olDepth--;
|
||||||
|
if (self->olDepth == 0) {
|
||||||
|
self->state = IN_NAV_TOC;
|
||||||
|
} else {
|
||||||
|
self->state = IN_LI; // Back to parent li
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
||||||
|
self->state = IN_BODY;
|
||||||
|
LOG_DBG("NAV", "Finished parsing nav toc");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Print.h>
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class BookMetadataCache;
|
||||||
|
|
||||||
|
// Parser for EPUB 3 nav.xhtml navigation documents
|
||||||
|
// Parses HTML5 nav elements with epub:type="toc" to extract table of contents
|
||||||
|
class TocNavParser final : public Print {
|
||||||
|
enum ParserState {
|
||||||
|
START,
|
||||||
|
IN_HTML,
|
||||||
|
IN_BODY,
|
||||||
|
IN_NAV_TOC, // Inside <nav epub:type="toc">
|
||||||
|
IN_OL, // Inside <ol>
|
||||||
|
IN_LI, // Inside <li>
|
||||||
|
IN_ANCHOR, // Inside <a>
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string& baseContentPath;
|
||||||
|
size_t remainingSize;
|
||||||
|
XML_Parser parser = nullptr;
|
||||||
|
ParserState state = START;
|
||||||
|
BookMetadataCache* cache;
|
||||||
|
|
||||||
|
// Track nesting depth for <ol> elements to determine TOC depth
|
||||||
|
uint8_t olDepth = 0;
|
||||||
|
// Current entry data being collected
|
||||||
|
std::string currentLabel;
|
||||||
|
std::string currentHref;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TocNavParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache)
|
||||||
|
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
|
~TocNavParser() override;
|
||||||
|
|
||||||
|
bool setup();
|
||||||
|
|
||||||
|
size_t write(uint8_t) override;
|
||||||
|
size_t write(const uint8_t* buffer, size_t size) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#include "TocNcxParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <XmlParserUtils.h>
|
||||||
|
|
||||||
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
|
bool TocNcxParser::setup() {
|
||||||
|
parser = XML_ParserCreate(nullptr);
|
||||||
|
if (!parser) {
|
||||||
|
LOG_DBG("TOC", "Couldn't allocate memory for parser");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XML_SetUserData(parser, this);
|
||||||
|
XML_SetElementHandler(parser, startElement, endElement);
|
||||||
|
XML_SetCharacterDataHandler(parser, characterData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TocNcxParser::~TocNcxParser() { destroyXmlParser(parser); }
|
||||||
|
|
||||||
|
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }
|
||||||
|
|
||||||
|
size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
||||||
|
if (!parser) return 0;
|
||||||
|
|
||||||
|
const uint8_t* currentBufferPos = buffer;
|
||||||
|
auto remainingInBuffer = size;
|
||||||
|
|
||||||
|
while (remainingInBuffer > 0) {
|
||||||
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
|
if (!buf) {
|
||||||
|
LOG_DBG("TOC", "Couldn't allocate memory for buffer");
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||||
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
|
LOG_DBG("TOC", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
|
destroyXmlParser(parser);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBufferPos += toRead;
|
||||||
|
remainingInBuffer -= toRead;
|
||||||
|
remainingSize -= toRead;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
|
// NOTE: We rely on navPoint label and content coming before any nested navPoints, this will be fine:
|
||||||
|
// <navPoint>
|
||||||
|
// <navLabel><text>Chapter 1</text></navLabel>
|
||||||
|
// <content src="ch1.html"/>
|
||||||
|
// <navPoint> ...nested... </navPoint>
|
||||||
|
// </navPoint>
|
||||||
|
//
|
||||||
|
// This will NOT:
|
||||||
|
// <navPoint>
|
||||||
|
// <navPoint> ...nested... </navPoint>
|
||||||
|
// <navLabel><text>Chapter 1</text></navLabel>
|
||||||
|
// <content src="ch1.html"/>
|
||||||
|
// </navPoint>
|
||||||
|
|
||||||
|
auto* self = static_cast<TocNcxParser*>(userData);
|
||||||
|
|
||||||
|
if (self->state == START && strcmp(name, "ncx") == 0) {
|
||||||
|
self->state = IN_NCX;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NCX && strcmp(name, "navMap") == 0) {
|
||||||
|
self->state = IN_NAV_MAP;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles both top-level and nested navPoints
|
||||||
|
if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) {
|
||||||
|
self->state = IN_NAV_POINT;
|
||||||
|
self->currentDepth++;
|
||||||
|
|
||||||
|
self->currentLabel.clear();
|
||||||
|
self->currentSrc.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_POINT && strcmp(name, "navLabel") == 0) {
|
||||||
|
self->state = IN_NAV_LABEL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_LABEL && strcmp(name, "text") == 0) {
|
||||||
|
self->state = IN_NAV_LABEL_TEXT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "src") == 0) {
|
||||||
|
self->currentSrc = atts[i + 1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||||
|
auto* self = static_cast<TocNcxParser*>(userData);
|
||||||
|
if (self->state == IN_NAV_LABEL_TEXT) {
|
||||||
|
self->currentLabel.append(s, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
|
||||||
|
auto* self = static_cast<TocNcxParser*>(userData);
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_LABEL_TEXT && strcmp(name, "text") == 0) {
|
||||||
|
self->state = IN_NAV_LABEL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_LABEL && strcmp(name, "navLabel") == 0) {
|
||||||
|
self->state = IN_NAV_POINT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_POINT && strcmp(name, "navPoint") == 0) {
|
||||||
|
self->currentDepth--;
|
||||||
|
if (self->currentDepth == 0) {
|
||||||
|
self->state = IN_NAV_MAP;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
|
||||||
|
// At this point (end of content tag), we likely have both Label (from previous tags) and Src.
|
||||||
|
// This is the safest place to push the data, assuming <navLabel> always comes before <content>.
|
||||||
|
// NCX spec says navLabel comes before content.
|
||||||
|
if (!self->currentLabel.empty() && !self->currentSrc.empty()) {
|
||||||
|
const std::string rawTarget = self->baseContentPath + self->currentSrc;
|
||||||
|
const size_t pos = rawTarget.find('#');
|
||||||
|
const std::string rawPath = pos == std::string::npos ? rawTarget : rawTarget.substr(0, pos);
|
||||||
|
std::string href = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(rawPath));
|
||||||
|
std::string anchor;
|
||||||
|
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
anchor = FsHelpers::decodeUriEscapes(rawTarget.substr(pos + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->cache) {
|
||||||
|
self->cache->createTocEntry(self->currentLabel, href, anchor, self->currentDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear them so we don't re-add them if there are weird XML structures
|
||||||
|
self->currentLabel.clear();
|
||||||
|
self->currentSrc.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Print.h>
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class BookMetadataCache;
|
||||||
|
|
||||||
|
class TocNcxParser final : public Print {
|
||||||
|
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
||||||
|
|
||||||
|
const std::string& baseContentPath;
|
||||||
|
size_t remainingSize;
|
||||||
|
XML_Parser parser = nullptr;
|
||||||
|
ParserState state = START;
|
||||||
|
BookMetadataCache* cache;
|
||||||
|
|
||||||
|
std::string currentLabel;
|
||||||
|
std::string currentSrc;
|
||||||
|
uint8_t currentDepth = 0;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache)
|
||||||
|
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
|
~TocNcxParser() override;
|
||||||
|
|
||||||
|
bool setup();
|
||||||
|
|
||||||
|
size_t write(uint8_t) override;
|
||||||
|
size_t write(const uint8_t* buffer, size_t size) override;
|
||||||
|
};
|
||||||
+245
-27
@@ -1,6 +1,7 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
|
#include <BuildScratch.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
||||||
|
|
||||||
|
// Appends the shaped visual form of every RTL token in `text` to `shapedOut`.
|
||||||
|
// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream,
|
||||||
|
// so the SD advance table must be warmed with the presentation forms as well as
|
||||||
|
// the logical codepoints — otherwise every RTL word measurement misses the fast
|
||||||
|
// path and falls through to onGlyphMiss(), which opens the .cpfont and reads
|
||||||
|
// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph.
|
||||||
|
// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so
|
||||||
|
// pure-LTR text pays almost nothing.
|
||||||
|
void appendShapedRtlTokens(const char* text, std::string& shapedOut) {
|
||||||
|
const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; };
|
||||||
|
std::string token;
|
||||||
|
std::string visual;
|
||||||
|
const char* p = text;
|
||||||
|
while (*p) {
|
||||||
|
while (*p && isBreak(*p)) ++p;
|
||||||
|
const char* start = p;
|
||||||
|
bool hasRtlBytes = false;
|
||||||
|
while (*p && !isBreak(*p)) {
|
||||||
|
const auto b = static_cast<unsigned char>(*p);
|
||||||
|
hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB);
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
if (!hasRtlBytes) continue;
|
||||||
|
token.assign(start, p - start);
|
||||||
|
if (BidiUtils::applyBidiVisual(token.c_str(), visual, static_cast<int>(BidiUtils::BidiBaseDir::AUTO))) {
|
||||||
|
shapedOut += visual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
||||||
@@ -57,7 +88,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
|||||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
||||||
auto it = sdCardFonts_.find(fontId);
|
auto it = sdCardFonts_.find(fontId);
|
||||||
if (it != sdCardFonts_.end()) {
|
if (it != sdCardFonts_.end()) {
|
||||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
std::string shaped;
|
||||||
|
appendShapedRtlTokens(utf8Text, shaped);
|
||||||
|
int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
@@ -71,7 +104,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::strin
|
|||||||
// Augment the persistent advance-only table for layout measurement.
|
// Augment the persistent advance-only table for layout measurement.
|
||||||
// The table survives across paragraphs/sections (capped per font), so
|
// The table survives across paragraphs/sections (capped per font), so
|
||||||
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
||||||
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
|
std::string shaped;
|
||||||
|
for (const auto& w : words) {
|
||||||
|
appendShapedRtlTokens(w.c_str(), shaped);
|
||||||
|
}
|
||||||
|
int missed =
|
||||||
|
it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||||
if (missed > 0) {
|
if (missed > 0) {
|
||||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||||
}
|
}
|
||||||
@@ -92,19 +130,46 @@ void GfxRenderer::begin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::releaseFrameBufferForBuild() {
|
void GfxRenderer::releaseFrameBufferForBuild() {
|
||||||
display.releaseFrameBuffers();
|
// Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so
|
||||||
|
// it cannot move and repeated loans cannot fragment the heap (the previous
|
||||||
|
// free+realloc model measurably decayed the max contiguous block over a
|
||||||
|
// session). The bytes are deposited in the build-scratch registry so
|
||||||
|
// memory-hungry build phases (e.g. InflateStream's tinfl state + window)
|
||||||
|
// can claim them instead of allocating.
|
||||||
|
uint32_t size = 0;
|
||||||
|
uint8_t* scratch = display.lendFrameBufferStorage(&size);
|
||||||
frameBuffer = nullptr;
|
frameBuffer = nullptr;
|
||||||
|
if (scratch) {
|
||||||
|
buildscratch::lend(scratch, size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GfxRenderer::restoreFrameBufferAfterBuild() {
|
bool GfxRenderer::restoreFrameBufferAfterBuild() {
|
||||||
if (!display.reallocFrameBuffers()) {
|
buildscratch::reclaim();
|
||||||
LOG_ERR("GFX", "Framebuffer realloc failed after build");
|
display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed
|
||||||
return false;
|
|
||||||
}
|
|
||||||
frameBuffer = display.getFrameBuffer();
|
frameBuffer = display.getFrameBuffer();
|
||||||
return frameBuffer != nullptr;
|
return frameBuffer != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) {
|
||||||
|
// Nesting guard: if the framebuffer is already lent out (an outer loan),
|
||||||
|
// stay inert so this end() cannot return storage the outer loan still owns.
|
||||||
|
if (!renderer_.hasFrameBuffer()) return;
|
||||||
|
renderer_.releaseFrameBufferForBuild();
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::FrameBufferLoan::end() {
|
||||||
|
if (!active_) return;
|
||||||
|
active_ = false;
|
||||||
|
if (!renderer_.restoreFrameBufferAfterBuild()) {
|
||||||
|
// Only reachable if the framebuffer never existed, which begin() already
|
||||||
|
// asserts against; kept as a backstop since running blind helps nobody.
|
||||||
|
LOG_ERR("GFX", "Framebuffer restore failed - restarting");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
||||||
|
|
||||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||||
@@ -148,6 +213,61 @@ static inline void rotateCoordinates(const GfxRenderer::Orientation orientation,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Output of screenRectToAlignedMemRect: a rectangle in panel-memory
|
||||||
|
// coordinates whose x and width are guaranteed to be multiples of 8 (the
|
||||||
|
// SDK's EInkDisplay::displayWindow alignment requirement). `valid == false`
|
||||||
|
// means the input was empty or fully outside the panel.
|
||||||
|
struct AlignedMemRect {
|
||||||
|
uint16_t x = 0;
|
||||||
|
uint16_t y = 0;
|
||||||
|
uint16_t w = 0;
|
||||||
|
uint16_t h = 0;
|
||||||
|
bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate a screen-coordinate rectangle (the coordinate system used by
|
||||||
|
// fillRect / drawText / the rest of the renderer's public API) into a
|
||||||
|
// panel-memory rectangle suitable for direct framebuffer indexing. Rotates
|
||||||
|
// the rectangle's two opposite corners with rotateCoordinates(), takes the
|
||||||
|
// bounding box (which naturally swaps width/height in Portrait /
|
||||||
|
// PortraitInverted), then snaps the x extent outward to multiples of 8 and
|
||||||
|
// clamps to panel bounds. Precondition: panel dims are multiples of 8 (true
|
||||||
|
// for the 800x480 panel), so clamping cannot re-break alignment.
|
||||||
|
static AlignedMemRect screenRectToAlignedMemRect(GfxRenderer::Orientation orientation, int sx, int sy, int sw, int sh,
|
||||||
|
uint16_t panelWidth, uint16_t panelHeight) {
|
||||||
|
AlignedMemRect out;
|
||||||
|
if (sw <= 0 || sh <= 0) return out;
|
||||||
|
|
||||||
|
int x0, y0, x1, y1;
|
||||||
|
rotateCoordinates(orientation, sx, sy, &x0, &y0, panelWidth, panelHeight);
|
||||||
|
rotateCoordinates(orientation, sx + sw - 1, sy + sh - 1, &x1, &y1, panelWidth, panelHeight);
|
||||||
|
|
||||||
|
const int memXLo = std::min(x0, x1);
|
||||||
|
const int memYLo = std::min(y0, y1);
|
||||||
|
const int memXHi = std::max(x0, x1) + 1; // exclusive upper bound
|
||||||
|
const int memYHi = std::max(y0, y1) + 1;
|
||||||
|
|
||||||
|
// Snap x outward to multiples of 8.
|
||||||
|
int alignedXLo = memXLo & ~0x7; // round down
|
||||||
|
int alignedXHi = (memXHi + 7) & ~0x7; // round up
|
||||||
|
|
||||||
|
if (alignedXLo < 0) alignedXLo = 0;
|
||||||
|
if (alignedXHi > panelWidth) alignedXHi = panelWidth;
|
||||||
|
int clampedYLo = memYLo;
|
||||||
|
int clampedYHi = memYHi;
|
||||||
|
if (clampedYLo < 0) clampedYLo = 0;
|
||||||
|
if (clampedYHi > panelHeight) clampedYHi = panelHeight;
|
||||||
|
|
||||||
|
if (alignedXHi <= alignedXLo || clampedYHi <= clampedYLo) return out;
|
||||||
|
|
||||||
|
out.x = static_cast<uint16_t>(alignedXLo);
|
||||||
|
out.y = static_cast<uint16_t>(clampedYLo);
|
||||||
|
out.w = static_cast<uint16_t>(alignedXHi - alignedXLo);
|
||||||
|
out.h = static_cast<uint16_t>(clampedYHi - clampedYLo);
|
||||||
|
out.valid = true;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
enum class TextRotation { None, Rotated90CW };
|
enum class TextRotation { None, Rotated90CW };
|
||||||
|
|
||||||
// Shared glyph rendering logic for normal and rotated text.
|
// Shared glyph rendering logic for normal and rotated text.
|
||||||
@@ -432,18 +552,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
uint32_t prevCp = 0;
|
uint32_t prevCp = 0;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
||||||
// Skip Hebrew Niqqud (vowel marks)
|
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||||
continue;
|
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||||
}
|
// marks stay centered, raised above the base or (kasra) at their
|
||||||
|
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||||
if (utf8IsCombiningMark(cp)) {
|
// the getGlyph lookup and skip them, as before.
|
||||||
|
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||||
if (!combiningGlyph) continue;
|
if (!combiningGlyph) continue;
|
||||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
const auto anchor = combiningMark::anchorFor(cp);
|
||||||
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left,
|
const int raiseBy =
|
||||||
combiningGlyph->width);
|
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||||
|
const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||||
|
combiningGlyph->left, combiningGlyph->width);
|
||||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -485,7 +608,6 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
namespace {
|
namespace {
|
||||||
const char* resolveVisualText(const char* text, std::string& visualBuffer, const BidiUtils::BidiBaseDir baseDir) {
|
const char* resolveVisualText(const char* text, std::string& visualBuffer, const BidiUtils::BidiBaseDir baseDir) {
|
||||||
if (!text || *text == '\0') return text;
|
if (!text || *text == '\0') return text;
|
||||||
if (baseDir == BidiUtils::BidiBaseDir::NONE) return text; // caller supplies visual order
|
|
||||||
|
|
||||||
if (baseDir != BidiUtils::BidiBaseDir::RTL) {
|
if (baseDir != BidiUtils::BidiBaseDir::RTL) {
|
||||||
// Byte-level scan: skip BiDi when no RTL script lead bytes are present.
|
// Byte-level scan: skip BiDi when no RTL script lead bytes are present.
|
||||||
@@ -1384,6 +1506,53 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
|||||||
display.displayBuffer(refreshMode, fadingFix);
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
|
||||||
|
// The async path has no turn-off-screen hook, which the sunlight fading fix
|
||||||
|
// relies on; keep those users on the blocking path.
|
||||||
|
if (fadingFix) {
|
||||||
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
display.displayBufferAsync(refreshMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
|
||||||
|
|
||||||
|
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
|
||||||
|
|
||||||
|
size_t GfxRenderer::readFramebufferRegion(int x, int y, int w, int h, uint8_t* dst, size_t dstCapacity) const {
|
||||||
|
if (dst == nullptr || w <= 0 || h <= 0) return 0;
|
||||||
|
|
||||||
|
const AlignedMemRect mem = screenRectToAlignedMemRect(orientation, x, y, w, h, panelWidth, panelHeight);
|
||||||
|
if (!mem.valid) return 0;
|
||||||
|
|
||||||
|
const size_t rowBytes = mem.w / 8; // exact: mem.w is a multiple of 8
|
||||||
|
const size_t needed = rowBytes * mem.h;
|
||||||
|
if (needed > dstCapacity) return 0;
|
||||||
|
|
||||||
|
for (uint16_t row = 0; row < mem.h; ++row) {
|
||||||
|
const uint8_t* srcRow = frameBuffer + (static_cast<uint32_t>(mem.y + row) * panelWidthBytes) + (mem.x / 8);
|
||||||
|
uint8_t* dstRow = dst + (static_cast<size_t>(row) * rowBytes);
|
||||||
|
memcpy(dstRow, srcRow, rowBytes);
|
||||||
|
}
|
||||||
|
return needed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::writeFramebufferRegion(int x, int y, int w, int h, const uint8_t* src) {
|
||||||
|
if (src == nullptr || w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
const AlignedMemRect mem = screenRectToAlignedMemRect(orientation, x, y, w, h, panelWidth, panelHeight);
|
||||||
|
if (!mem.valid) return;
|
||||||
|
|
||||||
|
const size_t rowBytes = mem.w / 8; // exact: mem.w is a multiple of 8
|
||||||
|
|
||||||
|
for (uint16_t row = 0; row < mem.h; ++row) {
|
||||||
|
const uint8_t* srcRow = src + (static_cast<size_t>(row) * rowBytes);
|
||||||
|
uint8_t* dstRow = frameBuffer + (static_cast<uint32_t>(mem.y + row) * panelWidthBytes) + (mem.x / 8);
|
||||||
|
memcpy(dstRow, srcRow, rowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
if (!text || maxWidth <= 0) return "";
|
if (!text || maxWidth <= 0) return "";
|
||||||
@@ -1497,6 +1666,35 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
return panelWidth;
|
return panelWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::tapToLogical(float nx, float ny, int& outX, int& outY) const {
|
||||||
|
int phyX = static_cast<int>(nx * panelWidth);
|
||||||
|
int phyY = static_cast<int>(ny * panelHeight);
|
||||||
|
if (phyX < 0) phyX = 0;
|
||||||
|
if (phyX > panelWidth - 1) phyX = panelWidth - 1;
|
||||||
|
if (phyY < 0) phyY = 0;
|
||||||
|
if (phyY > panelHeight - 1) phyY = panelHeight - 1;
|
||||||
|
|
||||||
|
switch (orientation) {
|
||||||
|
case Portrait:
|
||||||
|
outX = panelHeight - 1 - phyY;
|
||||||
|
outY = phyX;
|
||||||
|
break;
|
||||||
|
case PortraitInverted:
|
||||||
|
outX = phyY;
|
||||||
|
outY = panelWidth - 1 - phyX;
|
||||||
|
break;
|
||||||
|
case LandscapeClockwise:
|
||||||
|
outX = panelWidth - 1 - phyX;
|
||||||
|
outY = panelHeight - 1 - phyY;
|
||||||
|
break;
|
||||||
|
case LandscapeCounterClockwise:
|
||||||
|
default:
|
||||||
|
outX = phyX;
|
||||||
|
outY = phyY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Translate a logical rect through rotateCoordinates and take the bounding
|
// Translate a logical rect through rotateCoordinates and take the bounding
|
||||||
// box of its four corners on the physical panel. Output coords are inclusive
|
// box of its four corners on the physical panel. Output coords are inclusive
|
||||||
// and clamped. Returns false if the rect ends up fully off-panel.
|
// and clamped. Returns false if the rect ends up fully off-panel.
|
||||||
@@ -1630,6 +1828,15 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
||||||
|
// Measure the exact codepoint stream drawText renders: bidi-reordered and
|
||||||
|
// Arabic-shaped (contextual presentation forms, Lam-Alef collapse).
|
||||||
|
// Measuring the raw logical text counts the Alef a ligature absorbs and
|
||||||
|
// uses base-letter advances instead of presentation-form advances, so RTL
|
||||||
|
// lines come out wider than they draw — uneven word gaps and a ragged
|
||||||
|
// right margin.
|
||||||
|
std::string visual;
|
||||||
|
text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO);
|
||||||
|
|
||||||
// Advance table fast-path for SD card fonts during layout.
|
// Advance table fast-path for SD card fonts during layout.
|
||||||
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
||||||
// where kern/lig data was not loaded.
|
// where kern/lig data was not loaded.
|
||||||
@@ -1645,6 +1852,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
}
|
}
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
||||||
|
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||||
|
if (BidiUtils::isTransparentMark(cp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
||||||
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
||||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||||
@@ -1667,6 +1878,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
|
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||||
|
if (BidiUtils::isTransparentMark(cp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (utf8IsCombiningMark(cp)) {
|
if (utf8IsCombiningMark(cp)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1743,18 +1958,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
uint32_t prevCp = 0;
|
uint32_t prevCp = 0;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
// Skip Hebrew Niqqud (vowel marks)
|
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||||
continue;
|
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||||
}
|
// marks stay centered, raised above the base or (kasra) at their
|
||||||
|
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||||
if (utf8IsCombiningMark(cp)) {
|
// the getGlyph lookup and skip them, as before.
|
||||||
|
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||||
if (!combiningGlyph) continue;
|
if (!combiningGlyph) continue;
|
||||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
const auto anchor = combiningMark::anchorFor(cp);
|
||||||
|
const int raiseBy =
|
||||||
|
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||||
const int combiningX = x - raiseBy;
|
const int combiningX = x - raiseBy;
|
||||||
const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth,
|
const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||||
combiningGlyph->left, combiningGlyph->width);
|
combiningGlyph->left, combiningGlyph->width);
|
||||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ namespace BidiUtils {
|
|||||||
// AUTO: scan text for first strong directional character (P2/P3 rules)
|
// AUTO: scan text for first strong directional character (P2/P3 rules)
|
||||||
// LTR: force left-to-right paragraph embedding level
|
// LTR: force left-to-right paragraph embedding level
|
||||||
// RTL: force right-to-left paragraph embedding level
|
// RTL: force right-to-left paragraph embedding level
|
||||||
// NONE: text is already in visual order (e.g. FreeInkBook page runs, which
|
enum class BidiBaseDir : signed char { AUTO = -1, LTR = 0, RTL = 1 };
|
||||||
// bake UAX#9 reordering and Arabic shaping in at layout time) — draw
|
|
||||||
// byte-for-byte, never reorder
|
|
||||||
enum class BidiBaseDir : signed char { NONE = -2, AUTO = -1, LTR = 0, RTL = 1 };
|
|
||||||
} // namespace BidiUtils
|
} // namespace BidiUtils
|
||||||
|
|
||||||
class FontCacheManager;
|
class FontCacheManager;
|
||||||
@@ -137,7 +134,19 @@ class GfxRenderer {
|
|||||||
// Screen ops
|
// Screen ops
|
||||||
int getScreenWidth() const;
|
int getScreenWidth() const;
|
||||||
int getScreenHeight() const;
|
int getScreenHeight() const;
|
||||||
|
void tapToLogical(float nx, float ny, int& outX, int& outY) const;
|
||||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
|
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
|
||||||
|
// grayscale strip rendering) can overlap the panel's refresh time. The
|
||||||
|
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
|
||||||
|
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
|
||||||
|
// support. See HalDisplay::displayBufferAsync for the baseline contract.
|
||||||
|
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||||
|
void waitRefreshComplete() const;
|
||||||
|
// True when displayBufferAsync() genuinely overlaps: panel defers and
|
||||||
|
// fadingFix isn't forcing the blocking path. Callers can skip overlap
|
||||||
|
// scaffolding (e.g. whole-plane grayscale buffers) when false.
|
||||||
|
bool supportsAsyncRefresh() const;
|
||||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||||
// void displayWindow(int x, int y, int width, int height) const;
|
// void displayWindow(int x, int y, int width, int height) const;
|
||||||
void invertScreen() const;
|
void invertScreen() const;
|
||||||
@@ -192,6 +201,15 @@ class GfxRenderer {
|
|||||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||||
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||||
|
|
||||||
|
// Snapshot / restore a screen-coordinate framebuffer region (byte-aligned in
|
||||||
|
// panel memory). readFramebufferRegion returns the bytes written to dst, or
|
||||||
|
// 0 when the region is empty, offscreen, or exceeds dstCapacity. Pass the
|
||||||
|
// same rectangle to writeFramebufferRegion to restore the saved pixels.
|
||||||
|
// Enables partial-repaint patterns (e.g. moving a selection highlight)
|
||||||
|
// without re-rendering the whole page.
|
||||||
|
size_t readFramebufferRegion(int x, int y, int w, int h, uint8_t* dst, size_t dstCapacity) const;
|
||||||
|
void writeFramebufferRegion(int x, int y, int w, int h, const uint8_t* src);
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
||||||
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
|
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
|
||||||
@@ -253,15 +271,34 @@ class GfxRenderer {
|
|||||||
// Font helpers
|
// Font helpers
|
||||||
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
||||||
|
|
||||||
// Lend the 48 KB framebuffer to a memory-hungry phase (chapter builds).
|
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
|
||||||
// Between release and a successful restore NOTHING may draw or display —
|
// builds) WITHOUT freeing the allocation, so it never moves and repeated
|
||||||
// the panel keeps showing its last refreshed image. restore returns the
|
// loans cannot fragment the heap. Between release and restore NOTHING may
|
||||||
// buffer white, so the caller must redraw the full screen; false means the
|
// draw or display — the panel keeps showing its last refreshed image. The
|
||||||
// heap could not re-supply the buffer (callers treat that as fatal).
|
// lent bytes are published via buildscratch::claim() for consumers like
|
||||||
|
// InflateStream. restore returns the buffer white, so the caller must
|
||||||
|
// redraw the full screen; it cannot fail (no allocation involved).
|
||||||
void releaseFrameBufferForBuild();
|
void releaseFrameBufferForBuild();
|
||||||
bool restoreFrameBufferAfterBuild();
|
bool restoreFrameBufferAfterBuild();
|
||||||
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
|
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
|
||||||
|
|
||||||
|
// RAII form of the loan above, for blocking build regions with early-return
|
||||||
|
// error paths: restores on scope exit (or explicitly via end()). Display the
|
||||||
|
// popup/screen the panel should hold BEFORE constructing one. Constructing
|
||||||
|
// while the framebuffer is already lent yields an inert loan (nesting-safe).
|
||||||
|
class FrameBufferLoan {
|
||||||
|
public:
|
||||||
|
explicit FrameBufferLoan(GfxRenderer& renderer);
|
||||||
|
~FrameBufferLoan() { end(); }
|
||||||
|
void end();
|
||||||
|
FrameBufferLoan(const FrameBufferLoan&) = delete;
|
||||||
|
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GfxRenderer& renderer_;
|
||||||
|
bool active_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Low level functions
|
// Low level functions
|
||||||
uint8_t* getFrameBuffer() const;
|
uint8_t* getFrameBuffer() const;
|
||||||
size_t getBufferSize() const;
|
size_t getBufferSize() const;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user