Compare commits
131 Commits
0.16.0
...
crosspoint
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3853bfe113 | ||
|
|
fbe7d2feb4 | ||
|
|
520a0cb124 | ||
|
|
be8b02efd6 | ||
|
|
448ce55bb4 | ||
|
|
5464d9de3a | ||
|
|
48267ad848 | ||
|
|
dd630dcf72 | ||
|
|
ef705d3ac6 | ||
|
|
bab374a675 | ||
|
|
c171813045 | ||
|
|
d5e42b9e40 | ||
|
|
168c8fdb69 | ||
|
|
492cf976f5 | ||
|
|
25e255af50 | ||
|
|
a4adbb9dfe | ||
|
|
6ceba56620 | ||
|
|
62643ae933 | ||
|
|
8b41dccfb9 | ||
|
|
3204fa0339 | ||
|
|
bc6dc357eb | ||
|
|
ffe2aebd7e | ||
|
|
4965e63ad4 | ||
|
|
4db384edb6 | ||
|
|
f3075002c1 | ||
|
|
3e3be8bd23 | ||
|
|
800b07a2e5 | ||
|
|
2a31559747 | ||
|
|
c052512b1b | ||
|
|
bd95bfd44d | ||
|
|
fe446d4690 | ||
|
|
23e73312b4 | ||
|
|
e8d332e34f | ||
|
|
54004d5a5b | ||
|
|
d6e17c09ca | ||
|
|
7288e6499d | ||
|
|
5dab3ad5a3 | ||
|
|
82165c1022 | ||
|
|
e1fcec7d69 | ||
|
|
69a26ccb0e | ||
|
|
245d5a7dd8 | ||
|
|
e991fb10a6 | ||
|
|
4080184b27 | ||
|
|
8288cd2890 | ||
|
|
80c9e7a1d6 | ||
|
|
c2a966a6ea | ||
|
|
158caacfe0 | ||
|
|
1496ce68a6 | ||
|
|
0ab8e516f4 | ||
|
|
8687af738a | ||
|
|
a04388fd6c | ||
|
|
7349fbb208 | ||
|
|
9a723fead8 | ||
|
|
5fd1da5d2e | ||
|
|
0a2c661b8b | ||
|
|
8fb6402023 | ||
|
|
3b99459b82 | ||
|
|
1442521d0c | ||
|
|
7bbdf95aff | ||
|
|
76e9cf8f75 | ||
|
|
397abe1ef0 | ||
|
|
bc4edeef26 | ||
|
|
c90304f59b | ||
|
|
6ffd19a7e8 | ||
|
|
ff0392b9d2 | ||
|
|
03a18fb298 | ||
|
|
f01f3979bc | ||
|
|
3cee01b43d | ||
|
|
8920c62957 | ||
|
|
991b6b5a01 | ||
|
|
d8b8c5bad9 | ||
|
|
31199f9dd1 | ||
|
|
25b75b706f | ||
|
|
aa87b3f294 | ||
|
|
7a4af97ae8 | ||
|
|
703d95523b | ||
|
|
1a38fd96af | ||
|
|
5f4fa3bebe | ||
|
|
94a7c2c0b8 | ||
|
|
41eabba0d4 | ||
|
|
f739869519 | ||
|
|
99702a342c | ||
|
|
a707cc6da2 | ||
|
|
d8bee1d21f | ||
|
|
2f7312e6a0 | ||
|
|
d02e2e5b5e | ||
|
|
2c24ee3f81 | ||
|
|
cda8a5ec6d | ||
|
|
6e0cc4cf46 | ||
|
|
4d74cd795a | ||
|
|
08adc91bbe | ||
|
|
5996936c2e | ||
|
|
af58eb1987 | ||
|
|
e9e9ef68da | ||
|
|
91c8cc67ce | ||
|
|
fedc14bcb4 | ||
|
|
2b2bc95cf2 | ||
|
|
6bedc4ffec | ||
|
|
ecff988a29 | ||
|
|
c87a06edb8 | ||
|
|
7fce5b347d | ||
|
|
2f21f55512 | ||
|
|
1e20d30875 | ||
|
|
5c3828efe8 | ||
|
|
2952d7554c | ||
|
|
881f866d86 | ||
|
|
e3ae125f3c | ||
|
|
67494a7c90 | ||
|
|
59f493d293 | ||
|
|
ac1251282b | ||
|
|
16caa66b4a | ||
|
|
cf16d33710 | ||
|
|
51a4faddd4 | ||
|
|
a91bb0b1b8 | ||
|
|
481b8210fb | ||
|
|
c166b89f7b | ||
|
|
d5a9873bd7 | ||
|
|
6b533207e1 | ||
|
|
9493fb1f18 | ||
|
|
ff22a82563 | ||
|
|
1e506cce39 | ||
|
|
72fa6f8395 | ||
|
|
fd6ea01f64 | ||
|
|
8f3d226bf3 | ||
|
|
5c9412b141 | ||
|
|
750a6ee1d8 | ||
|
|
be2de1123b | ||
|
|
20b6d4d055 | ||
|
|
57379a6590 | ||
|
|
be10b90a71 | ||
|
|
94ce987f2c |
41
.gitea/workflows/ci.yml
Normal file
41
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,41 @@
|
||||
name: CI
|
||||
'on':
|
||||
push:
|
||||
branches: [master, crosspoint-ef]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
run: |
|
||||
# Use system Python on self-hosted runner
|
||||
python3 --version
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: python3 -m pip install --upgrade platformio
|
||||
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
- name: Run clang-format
|
||||
run: |
|
||||
# Use system clang-format if available, skip if not
|
||||
if command -v clang-format &> /dev/null; then
|
||||
./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
|
||||
else
|
||||
echo "clang-format not found, skipping format check"
|
||||
fi
|
||||
|
||||
- name: Generate Dictionary Index
|
||||
run: |
|
||||
python3 scripts/generate_dict_index.py --zip dict-en-en.zip --output lib/StarDict/DictPrefixIndex.generated.h
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: pio run
|
||||
40
.gitea/workflows/pr-formatting-check.yml
Normal file
40
.gitea/workflows/pr-formatting-check.yml
Normal file
@ -0,0 +1,40 @@
|
||||
name: "PR Formatting"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
title-check:
|
||||
name: Title Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Title Format
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
echo "Checking PR title: $PR_TITLE"
|
||||
|
||||
# Conventional commit pattern: type(scope): description or type: description
|
||||
# Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
||||
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_-]+\))?: .+"
|
||||
|
||||
if echo "$PR_TITLE" | grep -qE "$PATTERN"; then
|
||||
echo "✓ PR title follows conventional commit format"
|
||||
else
|
||||
echo "✗ PR title does not follow conventional commit format"
|
||||
echo ""
|
||||
echo "Expected format: type(scope): description"
|
||||
echo " or: type: description"
|
||||
echo ""
|
||||
echo "Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " feat(reader): add bookmark sync feature"
|
||||
echo " fix: resolve memory leak in epub parser"
|
||||
echo " docs: update README with new instructions"
|
||||
exit 1
|
||||
fi
|
||||
40
.gitea/workflows/release.yml
Normal file
40
.gitea/workflows/release.yml
Normal file
@ -0,0 +1,40 @@
|
||||
name: Compile Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
run: |
|
||||
# Use system Python on self-hosted runner
|
||||
python3 --version
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: python3 -m pip install --upgrade platformio
|
||||
|
||||
- name: Generate Dictionary Index
|
||||
run: |
|
||||
python3 scripts/generate_dict_index.py --zip dict-en-en.zip --output lib/StarDict/DictPrefixIndex.generated.h
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: pio run -e gh_release
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: CrossPoint-${{ github.ref_name }}
|
||||
path: |
|
||||
.pio/build/gh_release/bootloader.bin
|
||||
.pio/build/gh_release/firmware.bin
|
||||
.pio/build/gh_release/firmware.elf
|
||||
.pio/build/gh_release/firmware.map
|
||||
.pio/build/gh_release/partitions.bin
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@ -2,10 +2,20 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
.cursor/
|
||||
chat-summaries/
|
||||
lib/EpdFont/fontsrc
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
test/epubs/
|
||||
CrossPoint-ef.md
|
||||
Serial_print.code-search
|
||||
|
||||
# Gitea Release note drafts
|
||||
release-notes-*.md
|
||||
|
||||
# Gitea Actions runner config (contains credentials)
|
||||
.runner
|
||||
.runner.*
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@ -1,3 +1,5 @@
|
||||
[submodule "open-x4-sdk"]
|
||||
path = open-x4-sdk
|
||||
url = https://github.com/open-x4-epaper/community-sdk.git
|
||||
url = https://code.cottongin.xyz/cottongin/community-sdk.git
|
||||
branch = crosspoint-ef
|
||||
ignore = dirty
|
||||
|
||||
13
README.md
13
README.md
@ -1,4 +1,15 @@
|
||||
# CrossPoint Reader
|
||||
# CrossPoint Reader (ef fork)
|
||||
|
||||
> **Note:** This is **crosspoint-ef**, a heavily customized fork of [CrossPoint Reader](https://github.com/crosspoint-reader/crosspoint-reader) with additional features, UI improvements, and bug fixes. It also uses a [forked community-sdk](https://code.cottongin.xyz/cottongin/community-sdk) with additional hardware support.
|
||||
>
|
||||
> **Documentation:**
|
||||
> - [Feature Overview](./docs/crosspoint-ef-features.md) - What's new in this fork
|
||||
> - [User Guide](./docs/crosspoint-ef-user-guide.md) - How to use the new features
|
||||
> - [Technical Comparison](./docs/branch-comparison-summary.md) - Detailed diff from upstream
|
||||
>
|
||||
> **Disclaimer:** Much of the code in this fork was developed with assistance from [Claude](https://claude.ai), an AI assistant by Anthropic.
|
||||
|
||||
---
|
||||
|
||||
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
|
||||
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
|
||||
|
||||
137
claude_notes/ghosting-bisect-debug_2026-01-27_09-42-35.md
Normal file
137
claude_notes/ghosting-bisect-debug_2026-01-27_09-42-35.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Ghosting Issue Bisect Debug Summary
|
||||
|
||||
**Date:** 2026-01-27
|
||||
**Branch:** `catch-up-PR-merges`
|
||||
**Issue:** Text ghosting on page turns when anti-aliasing enabled
|
||||
|
||||
---
|
||||
|
||||
## Problem Description
|
||||
|
||||
After merging 15 upstream PRs, ghosting artifacts appeared when turning pages in the EPUB reader. The ghosting manifested as residual edges/outlines of previous page text, visible only when text anti-aliasing was enabled.
|
||||
|
||||
---
|
||||
|
||||
## The 15 Merged PRs (in merge order)
|
||||
|
||||
| Order | Commit | PR | Description |
|
||||
|-------|--------|-----|-------------|
|
||||
| 1 | `703d955` | #466 | fix: Add .vs folder to .gitignore |
|
||||
| 2 | `7a4af97` | #530 | docs: Update README with supported languages for EPUB |
|
||||
| 3 | `aa87b3f` | #547 | docs: add font generation commands to builtin font headers |
|
||||
| 4 | `25b75b7` | #425 | fix: Allow line break after ellipsis and underscore |
|
||||
| 5 | `31199f9` | #507 | fix: remove decimal places from progress % |
|
||||
| 6 | `d8b8c5b` | #526 | fix: add txt books to recent tab |
|
||||
| 7 | `991b6b5` | #498 | feat: treat .md files as .txt |
|
||||
| 8 | `8920c62` | #525 | fix: line break - flush word before br tag |
|
||||
| 9 | `3cee01b` | #460 | feat: add new configuration for front buttons |
|
||||
| 10 | `f01f397` | #557 | fix: rotate origin in drawImage |
|
||||
| 11 | `03a18fb` | #484 | UX improvement to Forget Network page |
|
||||
| 12 | `ff0392b` | #492 | fix: Validate settings on read |
|
||||
| 13 | `6ffd19a` | #482 | fix: short-press power button to wakeup |
|
||||
| 14 | `c90304f` | #465 | fix: cover artifacts - merge crop parameter |
|
||||
| 15 | `bc4edee` | #404 | Refactor: Replace CalibreWirelessActivity with CalibreConnectActivity |
|
||||
|
||||
**Base commit:** `1a38fd9` (before any PR merges)
|
||||
**Checkpoint commit:** `397abe1` (after all PR merges)
|
||||
|
||||
---
|
||||
|
||||
## Bisect Process
|
||||
|
||||
### Initial State
|
||||
- **GOOD:** `1a38fd9` - no ghosting
|
||||
- **BAD:** `397abe1` (HEAD) - ghosting present
|
||||
|
||||
### Bisect Steps
|
||||
|
||||
| Step | Commit | PR | Result | Remaining |
|
||||
|------|--------|-----|--------|-----------|
|
||||
| 1 | `991b6b5` | #498 (midpoint) | NO ghosting | Bug in commits 8-15 |
|
||||
| 2 | `ff0392b` | #492 (midpoint of 8-15) | NO ghosting | Bug in commits 13-15 |
|
||||
| 3 | `c90304f` | #465 | NO ghosting | Bug in commits 15 or checkpoint |
|
||||
| 4 | `bc4edee` | #404 | NO ghosting | Bug in checkpoint only |
|
||||
|
||||
### Compilation Issue During Bisect
|
||||
|
||||
A conflict from PR #526 merge left `RECENT_BOOKS.addBook()` with 1 argument instead of 3. This caused compilation failures at intermediate commits. Temporary fix applied at each step:
|
||||
|
||||
```cpp
|
||||
// Changed from:
|
||||
RECENT_BOOKS.addBook(txt->getPath());
|
||||
// To:
|
||||
RECENT_BOOKS.addBook(txt->getPath(), txt->getTitle(), "");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Finding
|
||||
|
||||
**The ghosting was NOT caused by any upstream PR.**
|
||||
|
||||
After testing all commits, the bisect pointed to the checkpoint commit `397abe1`. However, the diff between `bc4edee` and `397abe1` only contained:
|
||||
- The `addBook` fix (unrelated to display)
|
||||
- Removing duplicate `handleDownload` (unrelated to display)
|
||||
|
||||
### Actual Cause: Uncommitted Local Changes
|
||||
|
||||
The ghosting was caused by **uncommitted local changes in the IDE working directory**. These changes were being preserved across `git checkout` operations because they existed in IDE buffers.
|
||||
|
||||
When `git checkout -f catch-up-PR-merges` was executed (force checkout), all local changes were discarded and the ghosting disappeared.
|
||||
|
||||
---
|
||||
|
||||
## Local Changes That Were Present
|
||||
|
||||
The following modifications existed in the working directory but were NOT in the checkpoint commit:
|
||||
|
||||
- `CrossPointSettings.h/cpp` - enum `_COUNT` suffixes, `readAndValidate`, OPDS auth fields
|
||||
- `ChapterHtmlSlimParser.h/cpp` - `flushPartWordBuffer()` function
|
||||
- `SleepActivity.cpp` - `drawImage` coordinate changes
|
||||
- `JpegToBmpConverter.h/cpp` - `crop` parameter
|
||||
- `HomeActivity.cpp` - "OPDS Browser" label
|
||||
- `SettingsActivity.cpp` - front button layout options
|
||||
- `CrossPointWebServer.h/cpp` - UDP discovery, `WsUploadStatus`
|
||||
|
||||
**Important:** These changes were actually already committed in the PR merge commits. The confusion arose because:
|
||||
1. Checking out older commits removed these changes
|
||||
2. IDE buffers or manual re-application restored them as "local changes"
|
||||
3. This created a mismatch between committed code and working directory
|
||||
|
||||
---
|
||||
|
||||
## Resolution
|
||||
|
||||
1. Force checkout to HEAD: `git checkout -f catch-up-PR-merges`
|
||||
2. Verified all PR changes are properly committed
|
||||
3. Built and tested - no ghosting
|
||||
4. Working directory is now clean (matches committed state)
|
||||
|
||||
---
|
||||
|
||||
## Key Lessons
|
||||
|
||||
1. **Always check `git status` before bisecting** - local changes can persist across checkouts
|
||||
2. **Use `git checkout -f` or `git checkout <commit> -- .`** to ensure clean state
|
||||
3. **IDE buffers can reintroduce changes** - close/reload files after checkout if needed
|
||||
4. **Bisecting with compilation errors** requires temporary fixes that don't affect the bug being investigated
|
||||
5. **The "bug" may not be in commits at all** - it could be in uncommitted working directory changes
|
||||
|
||||
---
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Force checkout to discard local changes
|
||||
git checkout -f <branch>
|
||||
|
||||
# Checkout specific commit with clean state
|
||||
git checkout <commit> -- . && git checkout <commit>
|
||||
|
||||
# Check for uncommitted changes
|
||||
git status
|
||||
git diff --stat
|
||||
|
||||
# View what's in a specific commit
|
||||
git show <commit>:path/to/file
|
||||
```
|
||||
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Serial.printf Calls Without `if (Serial)` Guards
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Status:** Informational (not blocking issues)
|
||||
|
||||
## Summary
|
||||
|
||||
The codebase contains **408 Serial print calls** across 27 files in `src/`. Of these, only **16 calls** (in 2 files) have explicit `if (Serial)` guards.
|
||||
|
||||
**This is not a problem** because `Serial.setTxTimeoutMs(0)` is called in `setup()` before any activity code runs, making all Serial output non-blocking globally.
|
||||
|
||||
## Protection Mechanism
|
||||
|
||||
In `src/main.cpp` (lines 467-468):
|
||||
```cpp
|
||||
Serial.begin(115200);
|
||||
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||
```
|
||||
|
||||
This ensures that even without `if (Serial)` guards, Serial.printf calls will return immediately when USB is disconnected instead of blocking indefinitely.
|
||||
|
||||
## Files with `if (Serial)` Guards (16 calls)
|
||||
|
||||
| File | Protected Calls |
|
||||
|------|-----------------|
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | 15 |
|
||||
| `src/main.cpp` | 1 |
|
||||
|
||||
## Files Without Guards (392 calls)
|
||||
|
||||
These calls are protected by `Serial.setTxTimeoutMs(0)` but don't have explicit guards:
|
||||
|
||||
| File | Unguarded Calls |
|
||||
|------|-----------------|
|
||||
| `src/network/CrossPointWebServer.cpp` | 106 |
|
||||
| `src/activities/network/CrossPointWebServerActivity.cpp` | 49 |
|
||||
| `src/activities/boot_sleep/SleepActivity.cpp` | 33 |
|
||||
| `src/BookManager.cpp` | 25 |
|
||||
| `src/activities/reader/TxtReaderActivity.cpp` | 20 |
|
||||
| `src/activities/home/HomeActivity.cpp` | 16 |
|
||||
| `src/network/OtaUpdater.cpp` | 16 |
|
||||
| `src/util/Md5Utils.cpp` | 15 |
|
||||
| `src/main.cpp` | 13 (plus 1 guarded) |
|
||||
| `src/WifiCredentialStore.cpp` | 12 |
|
||||
| `src/network/HttpDownloader.cpp` | 12 |
|
||||
| `src/BookListStore.cpp` | 11 |
|
||||
| `src/activities/network/WifiSelectionActivity.cpp` | 11 |
|
||||
| `src/activities/settings/OtaUpdateActivity.cpp` | 9 |
|
||||
| `src/activities/browser/OpdsBookBrowserActivity.cpp` | 9 |
|
||||
| `src/activities/settings/ClearCacheActivity.cpp` | 7 |
|
||||
| `src/BookmarkStore.cpp` | 6 |
|
||||
| `src/RecentBooksStore.cpp` | 5 |
|
||||
| `src/activities/reader/ReaderActivity.cpp` | 4 |
|
||||
| `src/activities/Activity.h` | 3 |
|
||||
| `src/CrossPointSettings.cpp` | 3 |
|
||||
| `src/activities/network/CalibreConnectActivity.cpp` | 2 |
|
||||
| `src/activities/home/ListViewActivity.cpp` | 2 |
|
||||
| `src/activities/home/MyLibraryActivity.cpp` | 1 |
|
||||
| `src/activities/dictionary/DictionarySearchActivity.cpp` | 1 |
|
||||
| `src/CrossPointState.cpp` | 1 |
|
||||
|
||||
## Recommendation
|
||||
|
||||
No immediate action required. The global `Serial.setTxTimeoutMs(0)` protection is sufficient.
|
||||
|
||||
If desired, `if (Serial)` guards could be added to high-frequency logging paths for minor performance optimization (skipping format string processing), but this is low priority.
|
||||
|
||||
## Note on open-x4-sdk
|
||||
|
||||
The `open-x4-sdk` submodule also contains Serial calls (in `EInkDisplay.cpp`, `SDCardManager.cpp`). These are also protected by the global timeout setting since `Serial.begin()` and `setTxTimeoutMs()` are called before any SDK code executes.
|
||||
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Serial Blocking Debug Session Summary
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Issue:** Device freezes when booted without USB connected
|
||||
**Resolution:** `Serial.setTxTimeoutMs(0)` - make Serial TX non-blocking
|
||||
|
||||
## Problem Description
|
||||
|
||||
During release preparation for ef-0.15.9, the device was discovered to freeze completely when:
|
||||
1. Unplugged from USB
|
||||
2. Powered on via power button
|
||||
3. Book page displays, then device becomes unresponsive
|
||||
4. No button presses register
|
||||
|
||||
The device worked perfectly when USB was connected.
|
||||
|
||||
## Investigation Process
|
||||
|
||||
### Initial Hypotheses Tested
|
||||
|
||||
Multiple hypotheses were systematically investigated:
|
||||
|
||||
1. **Hypothesis A-D:** Display/rendering mutex issues
|
||||
- Added mutex logging to SD card
|
||||
- Mutex operations completed successfully
|
||||
- Ruled out as root cause
|
||||
|
||||
2. **Hypothesis E:** FreeRTOS task creation issues
|
||||
- Task created and ran successfully
|
||||
- First render completed normally
|
||||
- Ruled out
|
||||
|
||||
3. **Hypothesis F-G:** Main loop execution
|
||||
- Added loop counter logging to SD card
|
||||
- **Key finding:** Main loop never started logging
|
||||
- Setup() completed but loop() never executed meaningful work
|
||||
|
||||
4. **Hypothesis H-J:** Various timing and initialization issues
|
||||
- Tested different delays and initialization orders
|
||||
- No improvement
|
||||
|
||||
### Root Cause Discovery
|
||||
|
||||
The breakthrough came from analyzing the boot sequence:
|
||||
|
||||
1. `setup()` completes successfully
|
||||
2. `EpubReaderActivity::onEnter()` runs and calls `Serial.printf()` to log progress
|
||||
3. **Device hangs at Serial.printf() call**
|
||||
|
||||
On ESP32-C3 with USB CDC (USB serial), `Serial.printf()` blocks indefinitely waiting for the TX buffer to drain when USB is not connected. The default behavior expects a host to read the data.
|
||||
|
||||
### Evidence
|
||||
|
||||
- When USB connected: `Serial.printf()` returns immediately (data sent to host)
|
||||
- When USB disconnected: `Serial.printf()` blocks forever waiting for TX buffer space
|
||||
- The hang occurred specifically in `EpubReaderActivity.cpp` during progress logging
|
||||
|
||||
## Solution
|
||||
|
||||
### Primary Fix
|
||||
|
||||
Configure Serial to be non-blocking in `src/main.cpp`:
|
||||
|
||||
```cpp
|
||||
// Always initialize Serial but make it non-blocking
|
||||
Serial.begin(115200);
|
||||
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||
```
|
||||
|
||||
`Serial.setTxTimeoutMs(0)` tells the ESP32 Arduino core to return immediately from Serial write operations if the buffer is full, rather than blocking.
|
||||
|
||||
### Secondary Protection (Belt and Suspenders)
|
||||
|
||||
Added `if (Serial)` guards to high-traffic Serial calls in `EpubReaderActivity.cpp`:
|
||||
|
||||
```cpp
|
||||
if (Serial) Serial.printf("[%lu] [ERS] Loaded progress...\n", millis());
|
||||
```
|
||||
|
||||
This provides an additional check before attempting to print, though it's not strictly necessary with the timeout set to 0.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/main.cpp` | Added `Serial.setTxTimeoutMs(0)` after `Serial.begin()` |
|
||||
| `src/main.cpp` | Added `if (Serial)` guard to auto-sleep log |
|
||||
| `src/main.cpp` | Added `if (Serial)` guard to max loop duration log |
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | Added 16 `if (Serial)` guards |
|
||||
|
||||
## Verification
|
||||
|
||||
After applying the fix:
|
||||
1. Device boots successfully when unplugged from USB
|
||||
2. Book pages render correctly
|
||||
3. Button presses register normally
|
||||
4. Sleep/wake cycle works
|
||||
5. No functionality lost when USB is connected
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **ESP32-C3 USB CDC behavior:** Serial output can block indefinitely without a connected host
|
||||
2. **Always set non-blocking:** `Serial.setTxTimeoutMs(0)` should be standard for battery-powered devices
|
||||
3. **Debug logging location matters:** When debugging hangs, SD card logging proved essential since Serial was the problem
|
||||
4. **Systematic hypothesis testing:** Ruled out many red herrings (mutex, task, rendering) before finding the true cause
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Why This Affects ESP32-C3 Specifically
|
||||
|
||||
The ESP32-C3 uses native USB CDC for serial communication (no external USB-UART chip). The Arduino core's default behavior is to wait for TX buffer space, which requires an active USB host connection.
|
||||
|
||||
### Alternative Approaches Considered
|
||||
|
||||
1. **Only initialize Serial when USB connected:** Partially implemented, but insufficient because USB can be disconnected after boot
|
||||
2. **Add `if (Serial)` guards everywhere:** Too invasive (400+ calls)
|
||||
3. **Disable Serial entirely:** Would lose debug output when USB connected
|
||||
|
||||
The chosen solution (`setTxTimeoutMs(0)`) provides the best balance: debug output works when USB is connected, device operates normally when disconnected.
|
||||
|
||||
## References
|
||||
|
||||
- ESP32 Arduino Core Serial documentation
|
||||
- ESP-IDF USB CDC documentation
|
||||
- FreeRTOS queue behavior (initial red herring investigation)
|
||||
132
claude_notes/usb-serial-blocking-fix-2026-01-28.md
Normal file
132
claude_notes/usb-serial-blocking-fix-2026-01-28.md
Normal file
@ -0,0 +1,132 @@
|
||||
# USB Serial Blocking Issue - Root Cause and Fix
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Issue:** Device blocking/hanging when USB is not connected at boot
|
||||
|
||||
---
|
||||
|
||||
## Problem Description
|
||||
|
||||
The device would hang or behave unpredictably when booted without USB connected. This was traced to improper Serial handling on ESP32-C3 with USB CDC.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Factor A: `checkForFlashCommand()` Called Without Serial Initialization
|
||||
|
||||
The most critical issue was in `checkForFlashCommand()`, which is called at the start of every `loop()` iteration:
|
||||
|
||||
```cpp
|
||||
void loop() {
|
||||
checkForFlashCommand(); // Called EVERY loop iteration
|
||||
// ...
|
||||
}
|
||||
|
||||
void checkForFlashCommand() {
|
||||
while (Serial.available()) { // Called even when Serial.begin() was never called!
|
||||
char c = Serial.read();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When USB is not connected at boot, `Serial.begin()` is never called. Then in `loop()`, `checkForFlashCommand()` calls `Serial.available()` and `Serial.read()` on an uninitialized Serial object. On ESP32-C3 with USB CDC, this causes undefined behavior or blocking.
|
||||
|
||||
### Factor B: Removed `while (!Serial)` Wait Loop
|
||||
|
||||
The upstream 0.16.0 code included a 3-second wait loop after `Serial.begin()`:
|
||||
|
||||
```cpp
|
||||
if (isUsbConnected()) {
|
||||
Serial.begin(115200);
|
||||
unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 3000) {
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This wait loop was removed in an earlier attempt to fix boot delays, but it may be necessary for proper USB CDC initialization.
|
||||
|
||||
### Factor C: `Serial.setTxTimeoutMs(0)` Added Too Early
|
||||
|
||||
`Serial.setTxTimeoutMs(0)` was added immediately after `Serial.begin()` to make TX non-blocking. However, calling this before the Serial connection is fully established may interfere with USB CDC initialization.
|
||||
|
||||
---
|
||||
|
||||
## The Fix
|
||||
|
||||
### 1. Guard `checkForFlashCommand()` with Serial Check
|
||||
|
||||
```cpp
|
||||
void checkForFlashCommand() {
|
||||
if (!Serial) return; // Early exit if Serial not initialized
|
||||
while (Serial.available()) {
|
||||
// ... rest unchanged
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Restore Upstream Serial Initialization Pattern
|
||||
|
||||
```cpp
|
||||
void setup() {
|
||||
t1 = millis();
|
||||
|
||||
// Only start serial if USB connected
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
if (isUsbConnected()) {
|
||||
Serial.begin(115200);
|
||||
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
||||
unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 3000) {
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
// ... rest of setup
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Remove `Serial.setTxTimeoutMs(0)`
|
||||
|
||||
This call was removed entirely as it's not present in upstream and may cause issues.
|
||||
|
||||
### 4. Remove Unnecessary `if (Serial)` Guards
|
||||
|
||||
The 15 `if (Serial)` guards added to `EpubReaderActivity.cpp` were removed. `Serial.printf()` is safe to call when Serial isn't initialized (it simply returns 0), so guards around output calls are unnecessary.
|
||||
|
||||
**Key distinction:**
|
||||
- `Serial.printf()` / `Serial.println()` - Safe without guards (no-op when not initialized)
|
||||
- `Serial.available()` / `Serial.read()` - **MUST** be guarded (undefined behavior when not initialized)
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/main.cpp` | Removed `Serial.setTxTimeoutMs(0)`, restored `while (!Serial)` wait, added guard to `checkForFlashCommand()` |
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | Removed all 15 `if (Serial)` guards |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After applying fixes, verify:
|
||||
|
||||
1. ✅ Boot with USB connected, serial monitor open - should work
|
||||
2. ✅ Boot with USB connected, NO serial monitor - should work (3s delay then continue)
|
||||
3. ✅ Boot without USB - should work immediately (no blocking)
|
||||
4. ✅ Sleep without USB, plug in USB during sleep, wake - should work
|
||||
5. ✅ Sleep with USB, unplug during sleep, wake - should work
|
||||
|
||||
---
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Always guard Serial input operations**: `Serial.available()` and `Serial.read()` must be guarded with `if (Serial)` or `if (!Serial) return` when Serial initialization is conditional.
|
||||
|
||||
2. **Serial output is safe without guards**: `Serial.printf()` and similar output functions are safe to call even when Serial is not initialized - they simply return 0.
|
||||
|
||||
3. **Don't remove initialization waits without understanding why they exist**: The `while (!Serial)` wait loop exists for proper USB CDC initialization and shouldn't be removed without careful testing.
|
||||
|
||||
4. **Upstream patterns exist for a reason**: When diverging from upstream behavior, especially around low-level hardware initialization, be extra cautious and test thoroughly.
|
||||
BIN
dict-en-en.zip
Normal file
BIN
dict-en-en.zip
Normal file
Binary file not shown.
422
docs/branch-comparison-summary.md
Normal file
422
docs/branch-comparison-summary.md
Normal file
@ -0,0 +1,422 @@
|
||||
# Branch Comparison Summary: crosspoint-ef vs 0.16.0
|
||||
|
||||
This document provides a comprehensive comparison between the `crosspoint-ef` branch and the upstream `0.16.0` release for merge planning and implementation decisions.
|
||||
|
||||
## Branch History
|
||||
|
||||
| Branch | Base | Commits Since Base | Status |
|
||||
|--------|------|-------------------|--------|
|
||||
| `crosspoint-ef` | 0.15.0 | 90+ | Active development |
|
||||
| `0.16.0` | 0.15.0 | 30 | Released |
|
||||
|
||||
Both branches diverged from `0.15.0` at commit `3ce11f14`.
|
||||
|
||||
---
|
||||
|
||||
## Feature Comparison Matrix
|
||||
|
||||
### Major Features
|
||||
|
||||
| Feature | crosspoint-ef | 0.16.0 | Notes |
|
||||
|---------|:-------------:|:------:|-------|
|
||||
| Dictionary Support | Yes | No | StarDict format with word selection |
|
||||
| Bookmark System | Yes | No | Per-book bookmarks with visual indicator |
|
||||
| Quick Menu | Yes | No | Power button quick access |
|
||||
| Library Search | Yes | No | Character picker with weighted search |
|
||||
| CSS Parsing | Yes | No | Element, class, inline styles |
|
||||
| Inline Images (PNG/JPEG) | Yes | No | With caching and dithering |
|
||||
| Custom Fonts | Yes | No | Atkinson Hyperlegible, Fern Micro |
|
||||
| Enhanced Web Server | Yes | Partial | File ops, MD5 API, mDNS |
|
||||
| Companion App API | Yes | No | Deep links, WebSocket uploads |
|
||||
| Reading Lists | Yes | No | With pinning support |
|
||||
| Tab Bar Enhancements | Yes | No | Scrolling, overflow indicators |
|
||||
| High Contrast Mode | Yes | No | System-wide |
|
||||
| Bezel Compensation | Yes | No | Edge defect compensation |
|
||||
| Sleep Screen Edge Detection | Yes | No | Dominant color fill |
|
||||
| Recents Improvements | Yes | No | Badges, removal, clearing |
|
||||
| Progress Bar Status Bar | Yes | Yes | Same feature |
|
||||
| Spanish Hyphenation | No | Yes | Missing in crosspoint-ef |
|
||||
| XTC/XTCH Author Extraction | No | Yes | Missing in crosspoint-ef |
|
||||
| OTA Rework | No | Yes | Different implementation |
|
||||
| KOReader MD5 Binary Matching | No | Yes | Missing in crosspoint-ef |
|
||||
| Relative Position on Settings Change | No | Yes | Missing in crosspoint-ef |
|
||||
| Multi-line Keyboard Entry | No | Yes | Missing in crosspoint-ef |
|
||||
| Italics on Image Alt | No | Yes | Missing in crosspoint-ef |
|
||||
| Page Turn on Button Press (UX) | No | Yes | When chapter skip disabled |
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
| Fix | crosspoint-ef | 0.16.0 | Notes |
|
||||
|-----|:-------------:|:------:|-------|
|
||||
| Large EPUB indexing O(n²)→O(n) | Yes | Yes | Same fix |
|
||||
| Settings validation on read | Yes | Yes | Same fix |
|
||||
| Line break fixes | Yes | Yes | Similar fixes |
|
||||
| Rotate origin in drawImage | Yes | Yes | Same fix |
|
||||
| Short-press power wakeup | Yes | Yes | Same fix |
|
||||
| TXT books in recent tab | Yes | Yes | Same fix |
|
||||
| B&W filters for covers | Yes | Yes | Same fix |
|
||||
| Cover fit artifacts | Yes | Yes | Same fix |
|
||||
| Grayscale state corruption | Yes | No | Unique to crosspoint-ef |
|
||||
| Memory graceful degradation | Yes | No | Unique to crosspoint-ef |
|
||||
| Chapter Selection UI (KOReader) | No | Yes | Missing in crosspoint-ef |
|
||||
| Front layout in mapLabels() | No | Yes | Missing in crosspoint-ef |
|
||||
|
||||
---
|
||||
|
||||
## Files Changed Summary
|
||||
|
||||
### crosspoint-ef Unique Files (New)
|
||||
|
||||
| Category | Files |
|
||||
|----------|-------|
|
||||
| Dictionary | `src/activities/dictionary/` (8 files), `lib/StarDict/` (4 files) |
|
||||
| Bookmarks | `src/BookmarkStore.cpp/.h`, `src/activities/home/BookmarkListActivity.cpp/.h` |
|
||||
| Quick Menu | `src/activities/util/QuickMenuActivity.cpp/.h` |
|
||||
| CSS | `lib/Epub/Epub/css/` (3 files) |
|
||||
| Images | `lib/Epub/Epub/blocks/ImageBlock.cpp/.h`, `lib/Epub/Epub/converters/` (6 files) |
|
||||
| Custom Fonts | `src/customFonts.cpp`, `src/fontIds.h`, `lib/EpdFont/builtinFonts/custom/` (50+ files) |
|
||||
| Utils | `src/util/Md5Utils.cpp/.h`, `src/util/StringUtils.cpp/.h` |
|
||||
| Lists | `src/BookListStore.cpp/.h` |
|
||||
| Docs | `docs/webserver-api-reference.md`, `docs/companion-app-deep-link-API.md`, `docs/troubleshooting.md` |
|
||||
|
||||
### crosspoint-ef Modified Files (Significant Changes)
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/network/CrossPointWebServer.cpp` | +1083 lines (file ops, API, WebSocket) |
|
||||
| `src/activities/home/MyLibraryActivity.cpp` | +700 lines (tabs, search, badges) |
|
||||
| `src/main.cpp` | +255 lines (feature integration) |
|
||||
| `lib/GfxRenderer/GfxRenderer.cpp` | +439 lines (contrast, bezel) |
|
||||
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` | +596 lines (CSS integration) |
|
||||
| `src/CrossPointSettings.cpp/.h` | New settings fields |
|
||||
| `src/activities/boot_sleep/SleepActivity.cpp` | Edge detection, caching |
|
||||
| `src/RecentBooksStore.cpp` | Badges, removal, metadata |
|
||||
| `src/ScreenComponents.cpp` | Tab bar enhancements |
|
||||
|
||||
### 0.16.0 Unique Files/Changes
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h` | Spanish hyphenation (removed in ef) |
|
||||
| `lib/KOReaderSync/` | KOReader credential handling (removed in ef) |
|
||||
| `src/network/OtaUpdater.cpp` | OTA rework |
|
||||
|
||||
---
|
||||
|
||||
## Merge Strategy Recommendations
|
||||
|
||||
### Phase 1: Cherry-pick 0.16.0 Fixes into crosspoint-ef
|
||||
|
||||
**Low Risk - Recommended First:**
|
||||
|
||||
1. **Spanish hyphenation support** (#558)
|
||||
- Add `hyph-es.trie.h` back
|
||||
- Update `LanguageRegistry.cpp`
|
||||
|
||||
2. **Render keyboard entry over multiple lines** (#567)
|
||||
- Update `KeyboardEntryActivity.cpp`
|
||||
|
||||
3. **Correctly render italics on image alt** (#569)
|
||||
- Minimal change to text rendering
|
||||
|
||||
4. **Page turning on button pressed** (#451)
|
||||
- UX improvement when chapter skip disabled
|
||||
|
||||
5. **Missing front layout in mapLabels()** (#564)
|
||||
- Bug fix for button mapping
|
||||
|
||||
**Medium Risk:**
|
||||
|
||||
6. **KOReader document MD5 binary matching** (#529)
|
||||
- May conflict with MD5Utils changes
|
||||
|
||||
7. **Chapter Selection UI bugs** (#501)
|
||||
- Review for conflicts with tab bar changes
|
||||
|
||||
8. **Relative position on settings change** (#486)
|
||||
- Reader state management change
|
||||
|
||||
**Higher Risk:**
|
||||
|
||||
9. **OTA feature rework** (#509)
|
||||
- Compare implementations, may need reconciliation
|
||||
- crosspoint-ef has different OTA changes
|
||||
|
||||
10. **Extract author from XTC/XTCH** (#563)
|
||||
- XTC format was removed in crosspoint-ef
|
||||
- Evaluate if needed
|
||||
|
||||
### Phase 2: Potential Upstream Contributions from crosspoint-ef
|
||||
|
||||
**High Value, Moderate Complexity:**
|
||||
|
||||
1. **Dictionary Support**
|
||||
- Self-contained feature
|
||||
- New files, minimal integration points
|
||||
- Requires shipping dictionary data
|
||||
|
||||
2. **Bookmark System**
|
||||
- Clean implementation
|
||||
- New files with reader integration
|
||||
|
||||
3. **Quick Menu**
|
||||
- Simple overlay feature
|
||||
- Depends on bookmark and dictionary
|
||||
|
||||
4. **CSS Parsing**
|
||||
- Significant EPUB improvement
|
||||
- Well-isolated in `lib/Epub/Epub/css/`
|
||||
|
||||
**High Value, Higher Complexity:**
|
||||
|
||||
5. **Inline Image Support**
|
||||
- Major EPUB enhancement
|
||||
- Multiple new converters
|
||||
- Memory management considerations
|
||||
|
||||
6. **Library Search**
|
||||
- Integrated into MyLibraryActivity
|
||||
- Tab bar changes included
|
||||
|
||||
7. **Enhanced Web Server**
|
||||
- Large changes to CrossPointWebServer
|
||||
- New API endpoints
|
||||
- WebSocket uploads
|
||||
|
||||
**Medium Value:**
|
||||
|
||||
8. **Custom Fonts**
|
||||
- Large binary additions (font headers)
|
||||
- Clean integration
|
||||
|
||||
9. **Display Enhancements**
|
||||
- High contrast, bezel compensation
|
||||
- Settings additions
|
||||
|
||||
10. **Reading Lists**
|
||||
- New feature with web API
|
||||
|
||||
---
|
||||
|
||||
## Potential Conflicts
|
||||
|
||||
### High Conflict Risk
|
||||
|
||||
| Area | crosspoint-ef | 0.16.0 | Resolution |
|
||||
|------|---------------|--------|------------|
|
||||
| `MyLibraryActivity.cpp` | Major restructure | Minor fixes | Manual merge required |
|
||||
| `CrossPointWebServer.cpp` | Extensive additions | Minimal changes | crosspoint-ef likely compatible |
|
||||
| `CrossPointSettings.h` | Many new fields | Few changes | Additive, low conflict |
|
||||
| `main.cpp` | Feature integration | Minor changes | Review integration points |
|
||||
| `OtaUpdater.cpp` | Modified | Reworked (#509) | Compare implementations |
|
||||
|
||||
### Low Conflict Risk
|
||||
|
||||
| Area | Notes |
|
||||
|------|-------|
|
||||
| Dictionary files | All new, no conflicts |
|
||||
| Bookmark files | All new, no conflicts |
|
||||
| CSS parser files | All new, no conflicts |
|
||||
| Image converter files | All new, no conflicts |
|
||||
| Custom font files | All new, no conflicts |
|
||||
| StarDict library | All new, no conflicts |
|
||||
|
||||
---
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
### Regression Testing Required
|
||||
|
||||
After any merge:
|
||||
|
||||
1. **EPUB Reading**
|
||||
- Page navigation
|
||||
- Chapter selection
|
||||
- CSS styling
|
||||
- Image rendering
|
||||
- Bookmark indicators
|
||||
|
||||
2. **Library Functions**
|
||||
- Tab navigation
|
||||
- Search functionality
|
||||
- Recent books display
|
||||
- List management
|
||||
|
||||
3. **Dictionary**
|
||||
- Word selection
|
||||
- Lookup accuracy
|
||||
- Definition display
|
||||
|
||||
4. **Web Server**
|
||||
- File upload/download
|
||||
- API endpoints
|
||||
- WebSocket uploads
|
||||
- mDNS discovery
|
||||
|
||||
5. **Settings**
|
||||
- All new settings persist correctly
|
||||
- Settings migration from older versions
|
||||
|
||||
6. **Display**
|
||||
- High contrast mode
|
||||
- Bezel compensation (all orientations)
|
||||
- Sleep screen variations
|
||||
|
||||
### Memory Testing
|
||||
|
||||
crosspoint-ef includes memory optimization fixes. After merge:
|
||||
|
||||
1. Test with large EPUBs (2000+ chapters)
|
||||
2. Test opening multiple books in sequence
|
||||
3. Test anti-aliasing under memory pressure
|
||||
4. Monitor for ghosting/artifacts
|
||||
|
||||
---
|
||||
|
||||
## Priority Recommendations
|
||||
|
||||
### Immediate (For crosspoint-ef Stability)
|
||||
|
||||
1. Cherry-pick Spanish hyphenation (#558)
|
||||
2. Cherry-pick multi-line keyboard entry (#567)
|
||||
3. Cherry-pick italics on image alt (#569)
|
||||
4. Cherry-pick front layout fix (#564)
|
||||
|
||||
### Short-term (Feature Completeness)
|
||||
|
||||
5. Evaluate OTA rework (#509) - compare implementations
|
||||
6. Cherry-pick page turn UX (#451)
|
||||
7. Cherry-pick relative position fix (#486)
|
||||
|
||||
### Long-term (Upstream Contribution)
|
||||
|
||||
8. Prepare dictionary feature as PR
|
||||
9. Prepare bookmark system as PR
|
||||
10. Prepare CSS parsing as PR
|
||||
11. Evaluate inline image support for upstream
|
||||
|
||||
---
|
||||
|
||||
## File Inventory for Merge
|
||||
|
||||
### Files to Add to 0.16.0 Base (for upstream contribution)
|
||||
|
||||
```
|
||||
src/activities/dictionary/
|
||||
DictionaryMargins.h
|
||||
DictionaryMenuActivity.cpp
|
||||
DictionaryMenuActivity.h
|
||||
DictionaryResultActivity.cpp
|
||||
DictionaryResultActivity.h
|
||||
DictionarySearchActivity.cpp
|
||||
DictionarySearchActivity.h
|
||||
EpubWordSelectionActivity.cpp
|
||||
EpubWordSelectionActivity.h
|
||||
|
||||
src/activities/util/
|
||||
QuickMenuActivity.cpp
|
||||
QuickMenuActivity.h
|
||||
|
||||
src/activities/home/
|
||||
BookmarkListActivity.cpp
|
||||
BookmarkListActivity.h
|
||||
|
||||
src/
|
||||
BookmarkStore.cpp
|
||||
BookmarkStore.h
|
||||
BookListStore.cpp
|
||||
BookListStore.h
|
||||
customFonts.cpp
|
||||
fontIds.h
|
||||
BadgeConfig.h
|
||||
|
||||
src/util/
|
||||
Md5Utils.cpp
|
||||
Md5Utils.h
|
||||
StringUtils.cpp
|
||||
StringUtils.h
|
||||
|
||||
src/images/
|
||||
LockIcon.h
|
||||
|
||||
lib/StarDict/
|
||||
StarDict.cpp
|
||||
StarDict.h
|
||||
DictHtmlParser.cpp
|
||||
DictHtmlParser.h
|
||||
DictPrefixIndex.generated.h
|
||||
|
||||
lib/Epub/Epub/css/
|
||||
CssParser.cpp
|
||||
CssParser.h
|
||||
CssStyle.h
|
||||
|
||||
lib/Epub/Epub/blocks/
|
||||
ImageBlock.cpp
|
||||
ImageBlock.h
|
||||
BlockStyle.h
|
||||
|
||||
lib/Epub/Epub/converters/
|
||||
FramebufferWriter.cpp
|
||||
FramebufferWriter.h
|
||||
ImageDecoderFactory.cpp
|
||||
ImageDecoderFactory.h
|
||||
ImageToFramebufferDecoder.cpp
|
||||
ImageToFramebufferDecoder.h
|
||||
JpegToFramebufferConverter.cpp
|
||||
JpegToFramebufferConverter.h
|
||||
PngToFramebufferConverter.cpp
|
||||
PngToFramebufferConverter.h
|
||||
|
||||
lib/EpdFont/builtinFonts/custom/
|
||||
[All font header files]
|
||||
|
||||
docs/
|
||||
webserver-api-reference.md
|
||||
companion-app-deep-link-API.md
|
||||
troubleshooting.md
|
||||
crosspoint-ef-features.md
|
||||
crosspoint-ef-user-guide.md
|
||||
```
|
||||
|
||||
### Files to Merge Carefully
|
||||
|
||||
```
|
||||
src/main.cpp
|
||||
src/CrossPointSettings.cpp
|
||||
src/CrossPointSettings.h
|
||||
src/network/CrossPointWebServer.cpp
|
||||
src/network/CrossPointWebServer.h
|
||||
src/network/OtaUpdater.cpp
|
||||
src/network/OtaUpdater.h
|
||||
src/activities/home/MyLibraryActivity.cpp
|
||||
src/activities/home/MyLibraryActivity.h
|
||||
src/activities/reader/EpubReaderActivity.cpp
|
||||
src/activities/settings/SettingsActivity.cpp
|
||||
src/activities/settings/CategorySettingsActivity.cpp
|
||||
src/ScreenComponents.cpp
|
||||
src/ScreenComponents.h
|
||||
src/RecentBooksStore.cpp
|
||||
src/RecentBooksStore.h
|
||||
lib/GfxRenderer/GfxRenderer.cpp
|
||||
lib/GfxRenderer/GfxRenderer.h
|
||||
lib/GfxRenderer/BitmapHelpers.cpp
|
||||
lib/GfxRenderer/BitmapHelpers.h
|
||||
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
|
||||
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
|
||||
lib/Epub/Epub/Section.cpp
|
||||
lib/Epub/Epub/Section.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The `crosspoint-ef` branch represents a significant enhancement over the `0.15.0` baseline with 14+ major features. Most features are cleanly isolated in new files, making selective upstream contribution feasible.
|
||||
|
||||
**Recommended approach:**
|
||||
1. First, bring crosspoint-ef up to date with 0.16.0 bug fixes
|
||||
2. Then, evaluate individual features for upstream PR submission
|
||||
3. Prioritize dictionary, bookmarks, and CSS parsing as highest-value contributions
|
||||
|
||||
The grayscale state corruption fix in crosspoint-ef should also be submitted upstream as a critical bug fix, as it prevents display artifacts under memory pressure.
|
||||
309
docs/companion-app-deep-link-API.md
Normal file
309
docs/companion-app-deep-link-API.md
Normal file
@ -0,0 +1,309 @@
|
||||
# CrossPoint Companion Deep Link API
|
||||
|
||||
This document describes the deep link functionality that allows the CrossPoint Companion Android app to be launched from QR codes displayed on CrossPoint e-reader devices.
|
||||
|
||||
## Overview
|
||||
|
||||
The CrossPoint firmware can generate QR codes containing deep link URLs. When scanned with a mobile device, these URLs launch the companion app directly to a specific tab and optionally auto-connect to the device.
|
||||
|
||||
## URL Scheme
|
||||
|
||||
```
|
||||
crosspoint://<path>?<query_parameters>
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `crosspoint://` | Custom URL scheme registered by the app |
|
||||
| `<path>` | Target tab in the app (see [Path Mapping](#path-mapping)) |
|
||||
| `<query_parameters>` | Optional device connection parameters |
|
||||
|
||||
## Path Mapping
|
||||
|
||||
The URL path determines which tab the app navigates to:
|
||||
|
||||
| Path | App Tab | Description |
|
||||
|------|---------|-------------|
|
||||
| `files` | Device | File browser for device storage |
|
||||
| `library` | Library | Local book library |
|
||||
| `lists` | Lists | Reading lists management |
|
||||
| `settings` | Settings | App settings |
|
||||
|
||||
**Note:** Unknown paths default to the Library tab.
|
||||
|
||||
## Query Parameters
|
||||
|
||||
Query parameters provide device connection information for automatic connection:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `host` | string | *(required for auto-connect)* | IP address or hostname of the device |
|
||||
| `port` | integer | `80` | HTTP API port |
|
||||
| `wsPort` | integer | `81` | WebSocket port for file uploads |
|
||||
|
||||
## URL Examples
|
||||
|
||||
### Basic Navigation (No Auto-Connect)
|
||||
|
||||
Navigate to a specific tab without connecting to a device:
|
||||
|
||||
```
|
||||
crosspoint://files
|
||||
crosspoint://library
|
||||
crosspoint://lists
|
||||
crosspoint://settings
|
||||
```
|
||||
|
||||
### Auto-Connect to Device
|
||||
|
||||
Navigate to Device tab and auto-connect:
|
||||
|
||||
```
|
||||
crosspoint://files?host=192.168.1.100
|
||||
crosspoint://files?host=192.168.1.100&port=80&wsPort=81
|
||||
```
|
||||
|
||||
### Custom Ports
|
||||
|
||||
Connect to a device with non-default ports:
|
||||
|
||||
```
|
||||
crosspoint://files?host=192.168.1.100&port=8080&wsPort=8081
|
||||
```
|
||||
|
||||
### Hostname Instead of IP
|
||||
|
||||
```
|
||||
crosspoint://files?host=crosspoint.local&port=80&wsPort=81
|
||||
```
|
||||
|
||||
## Firmware Implementation
|
||||
|
||||
### QR Code Generation
|
||||
|
||||
The CrossPoint firmware should generate QR codes containing the deep link URL. Example format:
|
||||
|
||||
```
|
||||
crosspoint://files?host=<device_ip>&port=<http_port>&wsPort=<ws_port>
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<device_ip>` is the device's current IP address (e.g., from WiFi connection)
|
||||
- `<http_port>` is the HTTP API port (default: 80)
|
||||
- `<ws_port>` is the WebSocket port (default: 81)
|
||||
|
||||
### Example Firmware Code (C++)
|
||||
|
||||
```cpp
|
||||
String generateDeepLinkUrl(const char* path = "files") {
|
||||
String url = "crosspoint://";
|
||||
url += path;
|
||||
url += "?host=";
|
||||
url += WiFi.localIP().toString();
|
||||
url += "&port=";
|
||||
url += String(HTTP_PORT); // e.g., 80
|
||||
url += "&wsPort=";
|
||||
url += String(WS_PORT); // e.g., 81
|
||||
return url;
|
||||
}
|
||||
|
||||
// Generate QR code with:
|
||||
// String url = generateDeepLinkUrl("files");
|
||||
// displayQRCode(url);
|
||||
```
|
||||
|
||||
## App Behavior
|
||||
|
||||
### Launch Scenarios
|
||||
|
||||
#### 1. App Not Running
|
||||
|
||||
When the app is launched via deep link:
|
||||
1. App starts and parses the deep link URL
|
||||
2. Navigates to the target tab
|
||||
3. If device connection info is present and target is "files":
|
||||
- Checks for existing device with matching IP
|
||||
- If found: uses existing device (preserving user's custom name)
|
||||
- If not found: creates temporary connection
|
||||
- Attempts to connect automatically
|
||||
|
||||
#### 2. App Already Running
|
||||
|
||||
When a deep link is received while the app is open:
|
||||
1. `onNewIntent` receives the new URL
|
||||
2. Navigates to the target tab
|
||||
3. Handles device connection (same as above)
|
||||
|
||||
### Device Matching Logic
|
||||
|
||||
When connecting via deep link:
|
||||
|
||||
```
|
||||
1. Look up device by IP address in database
|
||||
2. If device exists:
|
||||
a. Check if ports match
|
||||
b. If ports differ, update the stored device with new ports
|
||||
c. Connect using the existing device (preserves custom name)
|
||||
3. If device doesn't exist:
|
||||
a. Create temporary Device object (not saved to database)
|
||||
b. Connect using temporary device
|
||||
c. Display as "CrossPoint (<ip>)"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Malformed URL | App opens to Library tab (default) |
|
||||
| Unknown path | App opens to Library tab with warning logged |
|
||||
| Invalid host format | Navigation succeeds, no auto-connect |
|
||||
| Invalid port values | Default ports used (80, 81) |
|
||||
| Connection failure | Error message displayed, user can retry |
|
||||
| Device unreachable | Error message with device IP shown |
|
||||
|
||||
## Android Implementation Details
|
||||
|
||||
### Intent Filter (AndroidManifest.xml)
|
||||
|
||||
```xml
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="crosspoint" />
|
||||
</intent-filter>
|
||||
```
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `DeepLinkParser` | Parses URI into `DeepLinkData` |
|
||||
| `DeepLinkData` | Data class holding parsed deep link info |
|
||||
| `DeviceConnectionInfo` | Data class for host/port/wsPort |
|
||||
| `MainActivity` | Handles incoming intents |
|
||||
| `CrossPointApp` | Routes navigation based on deep link |
|
||||
| `DeviceBrowserViewModel` | Handles `connectFromDeepLink()` |
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
QR Code Scan
|
||||
│
|
||||
▼
|
||||
Android Intent (ACTION_VIEW)
|
||||
│
|
||||
▼
|
||||
MainActivity.onCreate() / onNewIntent()
|
||||
│
|
||||
▼
|
||||
DeepLinkParser.parse(uri)
|
||||
│
|
||||
▼
|
||||
DeepLinkData { targetTab, deviceConnection? }
|
||||
│
|
||||
▼
|
||||
CrossPointApp (LaunchedEffect)
|
||||
│
|
||||
├─► Navigate to targetTab
|
||||
│
|
||||
└─► If targetTab == Device && deviceConnection != null
|
||||
│
|
||||
▼
|
||||
DeviceBrowserScreen
|
||||
│
|
||||
▼
|
||||
DeviceBrowserViewModel.connectFromDeepLink()
|
||||
│
|
||||
├─► Check existing device by IP
|
||||
├─► Update ports if needed
|
||||
└─► Connect and load files
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Host Validation
|
||||
|
||||
Valid hosts:
|
||||
- IPv4 addresses: `192.168.1.100`, `10.0.0.1`
|
||||
- Hostnames: `crosspoint.local`, `my-device`
|
||||
|
||||
Invalid hosts (rejected):
|
||||
- Empty strings
|
||||
- Malformed IPs: `192.168.1.256`, `192.168.1`
|
||||
- IPs with invalid octets
|
||||
|
||||
### Port Validation
|
||||
|
||||
- Valid range: 1-65535
|
||||
- Out-of-range values default to 80 (HTTP) or 81 (WebSocket)
|
||||
- Non-numeric values default to standard ports
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing with ADB
|
||||
|
||||
Test deep links without a QR code using ADB:
|
||||
|
||||
```bash
|
||||
# Basic navigation
|
||||
adb shell am start -a android.intent.action.VIEW -d "crosspoint://files"
|
||||
adb shell am start -a android.intent.action.VIEW -d "crosspoint://library"
|
||||
|
||||
# With device connection
|
||||
adb shell am start -a android.intent.action.VIEW -d "crosspoint://files?host=192.168.1.100"
|
||||
adb shell am start -a android.intent.action.VIEW -d "crosspoint://files?host=192.168.1.100&port=80&wsPort=81"
|
||||
|
||||
# Test while app is running (onNewIntent)
|
||||
adb shell am start -a android.intent.action.VIEW -d "crosspoint://settings"
|
||||
```
|
||||
|
||||
### Test Cases
|
||||
|
||||
1. **Valid deep link with connection info**
|
||||
- URL: `crosspoint://files?host=192.168.1.100&port=80&wsPort=81`
|
||||
- Expected: Opens Device tab, auto-connects to device
|
||||
|
||||
2. **Valid deep link without connection info**
|
||||
- URL: `crosspoint://files`
|
||||
- Expected: Opens Device tab, shows device selection
|
||||
|
||||
3. **Unknown path**
|
||||
- URL: `crosspoint://unknown`
|
||||
- Expected: Opens Library tab (default)
|
||||
|
||||
4. **Missing host parameter**
|
||||
- URL: `crosspoint://files?port=80`
|
||||
- Expected: Opens Device tab, no auto-connect
|
||||
|
||||
5. **Invalid host format**
|
||||
- URL: `crosspoint://files?host=invalid..host`
|
||||
- Expected: Opens Device tab, no auto-connect
|
||||
|
||||
6. **Device already exists in database**
|
||||
- Precondition: Device with IP 192.168.1.100 saved as "My Reader"
|
||||
- URL: `crosspoint://files?host=192.168.1.100`
|
||||
- Expected: Connects using "My Reader" name
|
||||
|
||||
7. **Existing device with different ports**
|
||||
- Precondition: Device saved with port=80, wsPort=81
|
||||
- URL: `crosspoint://files?host=192.168.1.100&port=8080&wsPort=8081`
|
||||
- Expected: Updates device ports, then connects
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Local Network Only**: Deep links should only contain local network addresses. The app does not validate this, but firmware should only generate URLs with local IPs.
|
||||
|
||||
2. **No Authentication**: The deep link does not include authentication. Device security relies on network-level access control.
|
||||
|
||||
3. **Temporary Devices**: Devices created from deep links (when no matching device exists) are not persisted, preventing automatic accumulation of device entries.
|
||||
|
||||
4. **No Sensitive Data**: Deep link URLs should not contain sensitive information as QR codes can be photographed.
|
||||
|
||||
## Changelog
|
||||
|
||||
| Version | Changes |
|
||||
|---------|---------|
|
||||
| 1.0.0 | Initial deep link support with `crosspoint://` scheme |
|
||||
603
docs/crosspoint-ef-features.md
Normal file
603
docs/crosspoint-ef-features.md
Normal file
@ -0,0 +1,603 @@
|
||||
# CrossPoint-EF Branch Features
|
||||
|
||||
This document describes the features and enhancements unique to the `crosspoint-ef` branch, which diverged from CrossPoint Reader at version `0.15.0`.
|
||||
|
||||
## Overview
|
||||
|
||||
The `crosspoint-ef` branch introduces significant new functionality including:
|
||||
|
||||
- **Dictionary Support** - Offline StarDict dictionary with word selection from reader
|
||||
- **Bookmark System** - Per-book bookmarks with visual indicators and management
|
||||
- **Quick Menu** - Fast access to common actions via power button
|
||||
- **Library Search** - Search across all books by title, author, or filename
|
||||
- **CSS Support** - Parse and apply CSS styles from EPUB files
|
||||
- **Inline Images** - PNG and Baseline JPEG image rendering within EPUBs
|
||||
- **Custom Fonts** - Atkinson Hyperlegible Next and Fern Micro accessibility fonts
|
||||
- **Enhanced Web Server** - File management, companion app API, mDNS discovery
|
||||
- **Reading Lists** - Create, manage, and pin custom book lists
|
||||
- **Display Enhancements** - High contrast mode, bezel compensation, sleep screen improvements
|
||||
|
||||
---
|
||||
|
||||
## Major Features
|
||||
|
||||
### 1. Dictionary Support
|
||||
|
||||
Full offline dictionary lookup using the StarDict format with fast prefix-indexed search.
|
||||
|
||||
**Features:**
|
||||
- Word selection directly from EPUB pages
|
||||
- Manual word entry via on-screen keyboard
|
||||
- Rich HTML formatting in definitions (bold, italic, lists)
|
||||
- Multi-page definitions with pagination
|
||||
- Synonym support
|
||||
- Case-insensitive search with prefix optimization
|
||||
|
||||
**Access Methods:**
|
||||
- **Quick Menu** → Dictionary
|
||||
- **Power Button** (when configured to `Dictionary` action)
|
||||
|
||||
**Dictionary Format:**
|
||||
- StarDict format with dictzip compression
|
||||
- Files located at `/dictionaries/dict-data` on SD card:
|
||||
- `dict-data.ifo` - Metadata
|
||||
- `dict-data.idx` - Word index
|
||||
- `dict-data.dict.dz` - Compressed definitions
|
||||
- `dict-data.syn` - Synonyms (optional)
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/activities/dictionary/`, `lib/StarDict/`
|
||||
- Prefix jump table for near-instant lookups
|
||||
- On-demand chunk decompression using miniz
|
||||
- HTML definition parsing with entity decoding
|
||||
|
||||
---
|
||||
|
||||
### 2. Bookmark System
|
||||
|
||||
Per-book bookmark storage with visual indicators and dedicated management interface.
|
||||
|
||||
**Features:**
|
||||
- Add/remove bookmarks from current page
|
||||
- Visual folded-corner indicator on bookmarked pages
|
||||
- Bookmarks tab in library showing all books with bookmarks
|
||||
- Long-press to delete bookmarks
|
||||
- Auto-generated bookmark names ("Chapter Title - Page X")
|
||||
- Maximum 100 bookmarks per book
|
||||
|
||||
**Storage:**
|
||||
- Binary file per book: `/.crosspoint/{epub_|txt_}<hash>/bookmarks.bin`
|
||||
- Stores: name, spine index, content offset, page number, timestamp
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/BookmarkStore.cpp/.h`, `src/activities/home/BookmarkListActivity.cpp/.h`
|
||||
- Bookmark identification by `spineIndex + contentOffset` (stable across re-renders)
|
||||
|
||||
---
|
||||
|
||||
### 3. Quick Menu
|
||||
|
||||
In-reader quick access menu for common actions, triggered by short power button press.
|
||||
|
||||
**Menu Options:**
|
||||
1. **Dictionary** - Look up a word
|
||||
2. **Bookmark** - Add/Remove bookmark (state-aware text)
|
||||
3. **Clear Cache** - Free up storage space
|
||||
4. **Settings** - Open settings menu
|
||||
|
||||
**Configuration:**
|
||||
- Settings → Controls → Short Power Button Click → `Quick Menu`
|
||||
|
||||
**Technical Implementation:**
|
||||
- File: `src/activities/util/QuickMenuActivity.cpp/.h`
|
||||
- Renders overlay with navigation and selection
|
||||
|
||||
---
|
||||
|
||||
### 4. Library Search with Character Picker
|
||||
|
||||
Search across all books using a dynamic character picker interface.
|
||||
|
||||
**Features:**
|
||||
- Character picker with dynamically generated character set from library content
|
||||
- Weighted search scoring:
|
||||
- Title match: 100 points (+50 if at start)
|
||||
- Author match: 80 points (+40 if at start)
|
||||
- Path match: 30 points
|
||||
- Results sorted by relevance score
|
||||
- Special controls: SPC (space), ← (backspace), CLR (clear)
|
||||
|
||||
**Navigation:**
|
||||
- Left/Right: Select character
|
||||
- Confirm: Add character to query
|
||||
- Up/Down: Switch between picker and results
|
||||
|
||||
**Technical Implementation:**
|
||||
- Integrated in `src/activities/home/MyLibraryActivity.cpp`
|
||||
- Search tab accessible from library tab bar
|
||||
|
||||
---
|
||||
|
||||
### 5. CSS Support for EPUBs
|
||||
|
||||
Parse and apply CSS styles from EPUB stylesheets.
|
||||
|
||||
**Supported Selectors:**
|
||||
- Element selectors: `p`, `div`, `h1`, etc.
|
||||
- Class selectors: `.classname`
|
||||
- Combined selectors: `element.classname`
|
||||
- Grouped selectors: `h1, h2, h3`
|
||||
- Inline styles: `style="..."`
|
||||
|
||||
**Supported Properties:**
|
||||
- `text-align` (left, center, right, justify)
|
||||
- `font-style` (normal, italic)
|
||||
- `font-weight` (normal, bold)
|
||||
- `text-decoration` (underline)
|
||||
- `text-indent`
|
||||
- `margin-top`, `margin-bottom`
|
||||
- `padding-top`, `padding-bottom`
|
||||
|
||||
**Cascade Order:**
|
||||
1. Element styles
|
||||
2. Class styles
|
||||
3. Element.class styles
|
||||
4. Inline styles
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `lib/Epub/Epub/css/CssParser.cpp/.h`, `CssStyle.h`
|
||||
- CSS files parsed during EPUB loading
|
||||
- Styles applied during HTML parsing via `ChapterHtmlSlimParser`
|
||||
|
||||
---
|
||||
|
||||
### 6. Inline Image Support (PNG/Baseline JPEG)
|
||||
|
||||
Render embedded images within EPUB content.
|
||||
|
||||
**Supported Formats:**
|
||||
- Baseline JPEG (.jpg, .jpeg)
|
||||
- PNG (.png)
|
||||
|
||||
**Features:**
|
||||
- Images decoded to 2-bit grayscale with dithering
|
||||
- Image caching as `.pxc` files (2 bits per pixel, packed format)
|
||||
- Row-by-row rendering to minimize memory usage
|
||||
- Automatic scaling to fit page width
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `lib/Epub/Epub/blocks/ImageBlock.cpp/.h`
|
||||
- Converters: `JpegToFramebufferConverter`, `PngToFramebufferConverter`
|
||||
- Factory: `ImageDecoderFactory` routes to appropriate decoder
|
||||
|
||||
---
|
||||
|
||||
### 7. Custom Fonts
|
||||
|
||||
Additional accessibility-focused fonts beyond the standard Bookerly and Noto Sans.
|
||||
|
||||
**Available Fonts:**
|
||||
1. **Atkinson Hyperlegible Next** - Designed for low-vision readers
|
||||
2. **Fern Micro** - Optimized for small screens
|
||||
|
||||
**Font Sizes:**
|
||||
- 12pt, 14pt, 16pt, 18pt for each font
|
||||
- Full style support: Regular, Italic, Bold, Bold Italic
|
||||
|
||||
**Configuration:**
|
||||
- Settings → Reader → Font Family → Custom
|
||||
- Settings → Reader → Custom Font → [Select font]
|
||||
- Settings → Reader → Fallback Font → [Bookerly/Noto Sans]
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/customFonts.cpp`, `src/fontIds.h`
|
||||
- Font headers: `lib/EpdFont/builtinFonts/custom/`
|
||||
- Conversion scripts: `lib/EpdFont/scripts/convert-builtin-fonts.sh`
|
||||
|
||||
---
|
||||
|
||||
### 8. Enhanced Web Server
|
||||
|
||||
Extended web server with file management operations and companion app support.
|
||||
|
||||
**File Operations:**
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/files` | GET | List files with MD5 hashes for EPUBs |
|
||||
| `/api/status` | GET | Device status (version, IP, heap, uptime) |
|
||||
| `/api/archived` | GET | List archived books |
|
||||
| `/api/hash` | GET | Compute/retrieve MD5 hash for sync |
|
||||
| `/download` | GET | Download files |
|
||||
| `/upload` | POST | Upload files (multipart) |
|
||||
| `/delete` | POST | Delete files/folders |
|
||||
| `/archive` | POST | Archive a book |
|
||||
| `/unarchive` | POST | Restore archived book |
|
||||
| `/rename` | POST | Rename files/folders |
|
||||
| `/copy` | POST | Copy files/folders |
|
||||
| `/move` | POST | Move files/folders |
|
||||
| `/mkdir` | POST | Create folders |
|
||||
| `/list` | GET/POST | Manage reading lists |
|
||||
|
||||
**WebSocket Upload (Port 81):**
|
||||
- Fast binary uploads for large files
|
||||
- Protocol: `START:<filename>:<size>:<path>` → `READY` → binary chunks → `DONE`
|
||||
- Progress updates: `PROGRESS:<received>:<total>`
|
||||
|
||||
**mDNS Discovery:**
|
||||
- Hostname: `crosspoint.local`
|
||||
- Service: `_http._tcp` on port 80
|
||||
- UDP discovery on port 8134
|
||||
|
||||
**Deep Link Support:**
|
||||
- URL scheme: `crosspoint://<path>?host=<ip>&port=<port>&wsPort=<wsPort>`
|
||||
- Paths: `files`, `library`, `lists`, `settings`
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/network/CrossPointWebServer.cpp/.h`
|
||||
- MD5 Utils: `src/util/Md5Utils.cpp/.h`
|
||||
- Docs: `docs/webserver-api-reference.md`, `docs/companion-app-deep-link-API.md`
|
||||
|
||||
---
|
||||
|
||||
### 9. Reading Lists with Pinning
|
||||
|
||||
Create and manage custom book lists with pinning support.
|
||||
|
||||
**Features:**
|
||||
- Create, load, delete lists
|
||||
- Pin a list to show on home screen
|
||||
- List contents displayed with book metadata
|
||||
- Web API for list upload/download (CSV format)
|
||||
|
||||
**Storage:**
|
||||
- Lists stored in `/.lists/` as `.bin` files
|
||||
- CSV format for API: `order,title,author,path`
|
||||
|
||||
**Configuration:**
|
||||
- Library → Lists tab → Long-press → Pin/Unpin
|
||||
- Pinned list name shown on home screen Lists button
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/BookListStore.cpp/.h`
|
||||
- Pinned list stored in `SETTINGS.pinnedListName`
|
||||
|
||||
---
|
||||
|
||||
### 10. Display Enhancements
|
||||
|
||||
#### High Contrast Mode
|
||||
|
||||
System-wide display contrast adjustment for improved readability.
|
||||
|
||||
- **Normal mode:** Standard grayscale thresholds
|
||||
- **High contrast mode:** Pushes mid-grays toward black/white
|
||||
|
||||
**Configuration:**
|
||||
- Settings → Display → High Contrast → On/Off
|
||||
|
||||
#### Bezel Compensation
|
||||
|
||||
Compensate for physical screen edge defects with configurable margin.
|
||||
|
||||
- **Range:** 0-10 pixels
|
||||
- **Edges:** Bottom, Top, Left, Right
|
||||
- **Behavior:** Margin rotates with screen orientation
|
||||
|
||||
**Configuration:**
|
||||
- Settings → Display → Bezel Compensation → [0-10]
|
||||
- Settings → Display → Bezel Edge → [Bottom/Top/Left/Right]
|
||||
|
||||
#### Sleep Screen Improvements
|
||||
|
||||
Enhanced sleep screen with edge-aware color filling.
|
||||
|
||||
- **Edge luminance detection:** Samples edge pixels for dominant color
|
||||
- **Letterbox fill:** Fills letterbox regions with edge colors for seamless appearance
|
||||
- **Two-level caching:**
|
||||
- Per-BMP cache: `.bmp.perim` files store edge luminance
|
||||
- Book-level cache: `edge.bin` stores cover data
|
||||
|
||||
---
|
||||
|
||||
### 11. Recents View Improvements
|
||||
|
||||
Enhanced recent books display with metadata and management.
|
||||
|
||||
**Features:**
|
||||
- **Badges:** Extension and suffix tags (e.g., "epub", "X4")
|
||||
- **Metadata display:** Title and author from EPUB
|
||||
- **Remove from recents:** Long-press → Remove from Recents
|
||||
- **Clear all:** Long-press → Clear All Recents
|
||||
|
||||
**Badge Configuration:**
|
||||
- Extension badges: `.epub` → "epub", `.txt` → "txt"
|
||||
- Suffix badges: `-x4` → "X4", `-x4p` → "X4P"
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/RecentBooksStore.cpp/.h`
|
||||
- Badge config: `src/BadgeConfig.h`
|
||||
- String utils: `src/util/StringUtils.cpp/.h`
|
||||
|
||||
---
|
||||
|
||||
### 12. Enhanced Tab Bar
|
||||
|
||||
Unified tab bar with scrolling and overflow indicators.
|
||||
|
||||
**Features:**
|
||||
- Horizontal scrolling when tabs exceed available width
|
||||
- Overflow indicators (< >) when content extends beyond view
|
||||
- Selected tab highlighting with underline
|
||||
- Bullet cursors for focus mode
|
||||
|
||||
**Tabs:**
|
||||
- Recent, Lists, Bookmarks, Search, Files
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/ScreenComponents.cpp/.h`
|
||||
- Used in `MyLibraryActivity` for library navigation
|
||||
|
||||
---
|
||||
|
||||
### 13. Progress Bar Status Bar
|
||||
|
||||
Additional status bar option showing visual progress bar.
|
||||
|
||||
**Options:**
|
||||
- `Full w/ Progress Bar` - Full status bar with progress bar
|
||||
- `Progress Bar` - Only progress bar, no other status info
|
||||
|
||||
**Configuration:**
|
||||
- Settings → Display → Status Bar
|
||||
|
||||
---
|
||||
|
||||
### 14. Additional Settings
|
||||
|
||||
New configuration options unique to crosspoint-ef:
|
||||
|
||||
| Setting | Options | Description |
|
||||
|---------|---------|-------------|
|
||||
| Short Power Button Click | Ignore, Sleep, Page Turn, Dictionary, Quick Menu | Map power button to action |
|
||||
| Bezel Compensation | 0-10 pixels | Edge defect compensation |
|
||||
| Bezel Edge | Bottom, Top, Left, Right | Which edge to compensate |
|
||||
| High Contrast | Off, On | System-wide contrast boost |
|
||||
| Custom Font | [Font list] | Select custom font |
|
||||
| Fallback Font | Bookerly, Noto Sans | Fallback for custom fonts |
|
||||
|
||||
---
|
||||
|
||||
### 15. OPDS Browser Enhancements
|
||||
|
||||
Improved OPDS catalog browsing experience.
|
||||
|
||||
**Features:**
|
||||
- **Navigation history stack** - Back button navigates through visited feeds
|
||||
- **Page skipping** - Hold Up/Down for 700ms to skip 23 items at once
|
||||
- **Error retry mechanism** - Retry button on error screens with WiFi status check
|
||||
- **HTTP Basic Authentication** - Username/password support for protected OPDS servers
|
||||
|
||||
**Configuration:**
|
||||
- Settings → System → Calibre Settings → OPDS Server URL
|
||||
- Settings → System → Calibre Settings → Username/Password
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/activities/browser/OpdsBookBrowserActivity.cpp`
|
||||
- Authentication: `src/network/HttpDownloader.cpp`
|
||||
|
||||
---
|
||||
|
||||
### 16. Development Tools
|
||||
|
||||
Scripts and utilities for firmware development.
|
||||
|
||||
**Scripts:**
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `pre_flash.py` | Displays "Flashing firmware..." screen during upload |
|
||||
| `debugging_monitor.py` | Enhanced serial monitor with memory graphs and color output |
|
||||
| `pio_helper.py` | Interactive PlatformIO workflow helper with presets |
|
||||
| `version_hash.py` | Embeds git commit hash in dev builds |
|
||||
| `build_html.py` | Minifies HTML files for web server |
|
||||
|
||||
**Firmware Flashing Screen:**
|
||||
- Full-screen display during firmware upload
|
||||
- Shows "Flashing firmware..." with version info
|
||||
- Lock icon indicates USB port location
|
||||
- Prevents accidental disconnection
|
||||
|
||||
**Debug/Memory Monitoring:**
|
||||
- `DEBUG_MEMORY` build mode for heap tracking at activity transitions
|
||||
- Periodic memory logging every 10 seconds (when Serial connected)
|
||||
- Loop duration warnings when exceeding 50ms
|
||||
- Detailed heap fragmentation info
|
||||
|
||||
**Technical Implementation:**
|
||||
- Scripts: `scripts/pre_flash.py`, `scripts/debugging_monitor.py`, `scripts/pio_helper.py`
|
||||
- Flash screen: `src/main.cpp` (lines 138-247)
|
||||
- Memory monitoring: `src/main.cpp` (lines 549-562)
|
||||
- Build config: `platformio.ini` (`debug_memory` environment)
|
||||
|
||||
---
|
||||
|
||||
### 17. Power Management Enhancements
|
||||
|
||||
Optimizations for battery life and responsiveness.
|
||||
|
||||
**Features:**
|
||||
- **Auto-sleep prevention** - Background tasks (web server, OTA, downloads) prevent auto-sleep
|
||||
- **USB connection detection** - Serial only starts when USB is connected (saves power)
|
||||
- **Skip loop delay** - Activities can request faster loop execution for responsive HTTP handling
|
||||
- **Power button release wait** - Prevents immediate wake if button is still held
|
||||
|
||||
**Technical Implementation:**
|
||||
- Files: `src/main.cpp`, `src/activities/Activity.h`
|
||||
- Methods: `preventAutoSleep()`, `skipLoopDelay()`
|
||||
|
||||
---
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
### Unique to crosspoint-ef
|
||||
|
||||
1. **Grayscale state corruption fix** - Prevents ghosting and gray filter artifacts when anti-aliasing is enabled under memory pressure
|
||||
2. **Memory optimization** - Graceful degradation when memory is low (skip anti-aliasing instead of corrupting display)
|
||||
|
||||
### Shared with 0.16.0
|
||||
|
||||
- Large EPUB indexing optimization (O(n²) → O(n))
|
||||
- Settings validation on read
|
||||
- Line break fixes (flush word before `<br/>`)
|
||||
- Rotate origin in `drawImage()`
|
||||
- Short-press power button to wakeup
|
||||
- Add txt books to recent tab
|
||||
- B&W filters for cover images
|
||||
|
||||
---
|
||||
|
||||
## Missing or Removed Features from 0.16.0
|
||||
|
||||
The following features are present in the upstream `0.16.0` release but are missing or were removed in `crosspoint-ef`:
|
||||
|
||||
### Removed Features
|
||||
|
||||
#### 1. KOReader Sync Support (Removed)
|
||||
|
||||
The entire KOReader sync functionality has been removed.
|
||||
|
||||
**What was removed:**
|
||||
- `lib/KOReaderSync/` library (8 files deleted)
|
||||
- Progress sync with KOReader sync server (`sync.koreader.rocks`)
|
||||
- Document MD5 binary matching for progress synchronization
|
||||
- KOReader credential storage
|
||||
|
||||
**Impact:**
|
||||
- Cannot sync reading progress with KOReader app
|
||||
- Chapter Selection UI fixes for KOReader sync (#501) not applicable
|
||||
|
||||
**Files deleted:**
|
||||
- `KOReaderSyncClient.cpp/.h`
|
||||
- `KOReaderCredentialStore.cpp/.h`
|
||||
- `KOReaderDocumentId.cpp/.h`
|
||||
- `ProgressMapper.cpp/.h`
|
||||
|
||||
---
|
||||
|
||||
#### 2. Non-English Hyphenation Patterns (Removed)
|
||||
|
||||
Hyphenation pattern files for non-English languages have been removed.
|
||||
|
||||
**What was removed:**
|
||||
- `hyph-es.trie.h` - Spanish hyphenation
|
||||
- `hyph-de.trie.h` - German hyphenation
|
||||
- `hyph-fr.trie.h` - French hyphenation
|
||||
- `hyph-ru.trie.h` - Russian hyphenation
|
||||
|
||||
**Impact:**
|
||||
- Only English hyphenation patterns remain
|
||||
- Non-English books will not hyphenate correctly
|
||||
- Spanish hyphenation support (#558) not available
|
||||
|
||||
**Note:** These can be restored by copying the trie files from 0.16.0.
|
||||
|
||||
---
|
||||
|
||||
#### 3. XTC/XTCH File Support (Removed)
|
||||
|
||||
Support for the XTC/XTCH proprietary format has been removed.
|
||||
|
||||
**What was removed:**
|
||||
- Author extraction from XTC/XTCH files (#563)
|
||||
- XTC format handling in file browsers
|
||||
|
||||
**Impact:**
|
||||
- XTC/XTCH files cannot be read
|
||||
- Author metadata not extracted from these formats
|
||||
|
||||
---
|
||||
|
||||
### Missing Bug Fixes
|
||||
|
||||
The following bug fixes from 0.16.0 have not been applied to crosspoint-ef:
|
||||
|
||||
| PR | Description | Impact |
|
||||
|----|-------------|--------|
|
||||
| #567 | Multi-line keyboard entry | Long text input truncated with "..." instead of wrapping |
|
||||
| #569 | Italics on image alt text | Image alt placeholders don't render in italics |
|
||||
| #564 | Front layout in mapLabels() | Button mapping may be incorrect in some layouts |
|
||||
| #486 | Relative position on settings change | Reader may jump to different location when settings change |
|
||||
| #501 | Chapter Selection UI (KOReader) | N/A - KOReader sync removed |
|
||||
| #529 | KOReader MD5 binary matching | N/A - KOReader sync removed |
|
||||
|
||||
---
|
||||
|
||||
### Missing UX Enhancements
|
||||
|
||||
| PR | Description | Impact |
|
||||
|----|-------------|--------|
|
||||
| #451 | Page turn on button press | When long-press chapter skip is disabled, 0.16.0 allows page turn on button press; crosspoint-ef does not |
|
||||
|
||||
---
|
||||
|
||||
### Different Implementation: OTA Updates
|
||||
|
||||
The OTA update mechanism uses a different implementation:
|
||||
|
||||
| Aspect | crosspoint-ef | 0.16.0 |
|
||||
|--------|---------------|--------|
|
||||
| HTTP Client | Arduino `HTTPClient` | ESP-IDF `esp_http_client` |
|
||||
| OTA Library | Arduino `Update` | ESP-IDF `esp_https_ota` |
|
||||
| Memory Management | Standard | Improved with custom buffer handling |
|
||||
|
||||
**Impact:**
|
||||
- Both implementations work, but 0.16.0's ESP-IDF approach may be more memory-efficient
|
||||
- Consider evaluating 0.16.0's OTA rework (#509) for potential adoption
|
||||
|
||||
---
|
||||
|
||||
### Recommendation for Missing Features
|
||||
|
||||
**High Priority to Cherry-pick:**
|
||||
1. Multi-line keyboard entry (#567) - Improves UX for long inputs
|
||||
2. Front layout fix (#564) - Bug fix for button mapping
|
||||
3. Relative position on settings change (#486) - Improves reader UX
|
||||
|
||||
**Medium Priority:**
|
||||
4. Restore hyphenation patterns for non-English languages
|
||||
5. Italics on image alt (#569) - Minor visual improvement
|
||||
6. Page turn on button press (#451) - UX enhancement
|
||||
|
||||
**Evaluate:**
|
||||
7. OTA rework (#509) - Compare implementations for memory benefits
|
||||
8. KOReader sync - Restore if sync functionality is desired
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
| Feature | Primary Files |
|
||||
|---------|---------------|
|
||||
| Dictionary | `src/activities/dictionary/`, `lib/StarDict/` |
|
||||
| Bookmarks | `src/BookmarkStore.*`, `src/activities/home/BookmarkListActivity.*` |
|
||||
| Quick Menu | `src/activities/util/QuickMenuActivity.*` |
|
||||
| Search | `src/activities/home/MyLibraryActivity.cpp` |
|
||||
| CSS | `lib/Epub/Epub/css/` |
|
||||
| Images | `lib/Epub/Epub/blocks/ImageBlock.*`, `lib/Epub/Epub/converters/` |
|
||||
| Custom Fonts | `src/customFonts.cpp`, `lib/EpdFont/builtinFonts/custom/` |
|
||||
| Web Server | `src/network/CrossPointWebServer.*`, `src/util/Md5Utils.*` |
|
||||
| Lists | `src/BookListStore.*` |
|
||||
| Settings | `src/CrossPointSettings.*` |
|
||||
| Tab Bar | `src/ScreenComponents.*` |
|
||||
| Recents | `src/RecentBooksStore.*`, `src/BadgeConfig.h` |
|
||||
| OPDS Browser | `src/activities/browser/OpdsBookBrowserActivity.*` |
|
||||
| Dev Tools | `scripts/pre_flash.py`, `scripts/debugging_monitor.py`, `scripts/pio_helper.py` |
|
||||
| Power Management | `src/main.cpp`, `src/activities/Activity.h` |
|
||||
|
||||
---
|
||||
|
||||
## Version Information
|
||||
|
||||
- **Base version:** 0.15.0
|
||||
- **Branch:** crosspoint-ef
|
||||
- **Commits since divergence:** 90+
|
||||
- **Files changed:** 250+
|
||||
555
docs/crosspoint-ef-user-guide.md
Normal file
555
docs/crosspoint-ef-user-guide.md
Normal file
@ -0,0 +1,555 @@
|
||||
# CrossPoint-EF User Guide Supplement
|
||||
|
||||
This guide covers the additional features available in the `crosspoint-ef` branch. For basic operation, refer to the main [User Guide](../USER_GUIDE.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Dictionary](#dictionary)
|
||||
- [Bookmarks](#bookmarks)
|
||||
- [Quick Menu](#quick-menu)
|
||||
- [Library Search](#library-search)
|
||||
- [Reading Lists](#reading-lists)
|
||||
- [Display Settings](#display-settings)
|
||||
- [Web Server Features](#web-server-features)
|
||||
- [Custom Fonts](#custom-fonts)
|
||||
- [Additional Settings](#additional-settings)
|
||||
|
||||
---
|
||||
|
||||
## Dictionary
|
||||
|
||||
The dictionary feature provides offline word lookup while reading.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Download a StarDict dictionary (English-English dictionary provided as `dict-en-en.zip`)
|
||||
2. Extract the dictionary files to `/dictionaries/dict-data/` on your SD card
|
||||
3. You should have these files:
|
||||
- `dict-data.ifo`
|
||||
- `dict-data.idx`
|
||||
- `dict-data.dict.dz`
|
||||
- `dict-data.syn` (optional, for synonyms)
|
||||
|
||||
### Using the Dictionary
|
||||
|
||||
#### Method 1: Quick Menu
|
||||
|
||||
1. While reading, press the **Power** button briefly (requires Quick Menu to be configured)
|
||||
2. Select **Dictionary** from the menu
|
||||
3. Choose **Select from Screen** or **Enter a Word**
|
||||
|
||||
#### Method 2: Direct Power Button Access
|
||||
|
||||
1. Go to **Settings → Controls → Short Power Button Click**
|
||||
2. Set to **Dictionary**
|
||||
3. While reading, press the **Power** button briefly to open the dictionary
|
||||
|
||||
### Selecting a Word from the Page
|
||||
|
||||
1. Choose **Select from Screen** from the dictionary menu
|
||||
2. The current page will display with word selection enabled
|
||||
3. Use **Left/Right** to move between words
|
||||
4. Use **Up/Down** to jump between lines
|
||||
5. Press **Confirm** to look up the selected word
|
||||
6. Press **Back** to cancel
|
||||
|
||||
### Viewing Definitions
|
||||
|
||||
- Definitions display with rich formatting (bold, italic, lists)
|
||||
- Use **Left/Right** or **Volume Up/Down** to navigate between pages if the definition is long
|
||||
- Press **Confirm** to search for another word
|
||||
- Press **Back** to return to your book
|
||||
|
||||
---
|
||||
|
||||
## Bookmarks
|
||||
|
||||
Create and manage bookmarks within your books.
|
||||
|
||||
### Adding a Bookmark
|
||||
|
||||
#### Method 1: Quick Menu
|
||||
|
||||
1. Press the **Power** button briefly (requires Quick Menu to be configured)
|
||||
2. Select **Add Bookmark** (or **Remove Bookmark** if already bookmarked)
|
||||
|
||||
#### Method 2: Settings Configuration
|
||||
|
||||
1. Go to **Settings → Controls → Short Power Button Click**
|
||||
2. Set to **Quick Menu**
|
||||
3. Use Quick Menu to toggle bookmarks
|
||||
|
||||
### Bookmark Indicator
|
||||
|
||||
When a page is bookmarked, a small folded corner triangle appears in the top-right corner of the page.
|
||||
|
||||
### Viewing Bookmarks
|
||||
|
||||
1. Go to **Home → Library**
|
||||
2. Select the **Bookmarks** tab
|
||||
3. You'll see a list of books that have bookmarks
|
||||
4. Select a book to view its bookmarks
|
||||
5. Select a bookmark to jump to that location
|
||||
|
||||
### Deleting Bookmarks
|
||||
|
||||
1. Open a book's bookmark list (from Bookmarks tab)
|
||||
2. Navigate to the bookmark you want to delete
|
||||
3. **Long-press Confirm** (hold for about 1 second)
|
||||
4. Confirm deletion when prompted
|
||||
|
||||
### Bookmark Naming
|
||||
|
||||
Bookmarks are automatically named based on:
|
||||
- Chapter title and page number (e.g., "Chapter 3 - Page 42")
|
||||
- Just page number if no chapter title (e.g., "Page 15")
|
||||
|
||||
---
|
||||
|
||||
## Quick Menu
|
||||
|
||||
Fast access to common actions while reading.
|
||||
|
||||
### Enabling Quick Menu
|
||||
|
||||
1. Go to **Settings → Controls → Short Power Button Click**
|
||||
2. Select **Quick Menu**
|
||||
|
||||
### Using Quick Menu
|
||||
|
||||
1. While reading, press the **Power** button briefly
|
||||
2. Navigate with **Up/Down** or **Left/Right**
|
||||
3. Press **Confirm** to select an option
|
||||
4. Press **Back** to close the menu
|
||||
|
||||
### Quick Menu Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| **Dictionary** | Look up a word |
|
||||
| **Add/Remove Bookmark** | Toggle bookmark on current page |
|
||||
| **Clear Cache** | Free up storage space |
|
||||
| **Settings** | Open settings menu |
|
||||
|
||||
---
|
||||
|
||||
## Library Search
|
||||
|
||||
Search your library by title, author, or filename.
|
||||
|
||||
### Accessing Search
|
||||
|
||||
1. Go to **Home → Library**
|
||||
2. Select the **Search** tab
|
||||
3. Or from any tab, scroll to the bottom and select **Search...**
|
||||
|
||||
### Using the Character Picker
|
||||
|
||||
The search uses a character picker interface:
|
||||
|
||||
1. **Left/Right** - Move between characters
|
||||
2. **Confirm** - Add character to search query
|
||||
3. **SPC** - Add a space
|
||||
4. **←** - Delete last character (backspace)
|
||||
5. **CLR** - Clear entire query
|
||||
|
||||
### Navigating Results
|
||||
|
||||
1. After entering characters, results appear below
|
||||
2. Press **Down** to move from character picker to results
|
||||
3. **Left/Right** to navigate results
|
||||
4. **Confirm** to open a book
|
||||
5. **Up** to return to character picker
|
||||
|
||||
### Search Scoring
|
||||
|
||||
Results are ranked by relevance:
|
||||
- Title matches rank highest
|
||||
- Author matches rank second
|
||||
- Filename matches rank lowest
|
||||
- Matches at the start of a field rank higher
|
||||
|
||||
---
|
||||
|
||||
## Reading Lists
|
||||
|
||||
Create custom book lists for organizing your library.
|
||||
|
||||
### Viewing Lists
|
||||
|
||||
1. Go to **Home → Library**
|
||||
2. Select the **Lists** tab
|
||||
3. Available lists are displayed
|
||||
|
||||
### Opening a List
|
||||
|
||||
1. Navigate to a list name
|
||||
2. Press **Confirm** to view the list contents
|
||||
3. Select a book to start reading
|
||||
|
||||
### Pinning a List
|
||||
|
||||
Pin a list to quickly access it from the home screen:
|
||||
|
||||
1. In the Lists tab, navigate to a list
|
||||
2. **Long-press Confirm** to open the action menu
|
||||
3. Select **Pin List**
|
||||
|
||||
The pinned list name will appear on the Lists button on the home screen.
|
||||
|
||||
### Unpinning a List
|
||||
|
||||
1. Navigate to the pinned list
|
||||
2. **Long-press Confirm**
|
||||
3. Select **Unpin List**
|
||||
|
||||
### Deleting a List
|
||||
|
||||
1. Navigate to a list
|
||||
2. **Long-press Confirm**
|
||||
3. Select **Delete List**
|
||||
4. Confirm deletion
|
||||
|
||||
### Creating Lists via Web Server
|
||||
|
||||
Lists can be created and uploaded via the web server API. See [Web Server Features](#web-server-features).
|
||||
|
||||
---
|
||||
|
||||
## Display Settings
|
||||
|
||||
### High Contrast Mode
|
||||
|
||||
Increases contrast across the entire UI for better readability.
|
||||
|
||||
1. Go to **Settings → Display → High Contrast**
|
||||
2. Set to **On** or **Off**
|
||||
|
||||
When enabled, mid-gray tones are pushed toward black or white.
|
||||
|
||||
### Bezel Compensation
|
||||
|
||||
Compensate for physical screen edge defects (common on some devices).
|
||||
|
||||
1. Go to **Settings → Display → Bezel Compensation**
|
||||
2. Set value from **0** (disabled) to **10** pixels
|
||||
3. If compensation is enabled, select **Bezel Edge**:
|
||||
- **Bottom** - Default, compensates bottom edge
|
||||
- **Top** - Compensates top edge
|
||||
- **Left** - Compensates left edge
|
||||
- **Right** - Compensates right edge
|
||||
|
||||
The compensation margin automatically rotates with screen orientation.
|
||||
|
||||
### Status Bar Options
|
||||
|
||||
Additional status bar display options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| None | No status bar |
|
||||
| No Progress | Status bar without reading progress |
|
||||
| Full w/ Percentage | Status bar with percentage progress |
|
||||
| Full w/ Progress Bar | Status bar with visual progress bar |
|
||||
| Progress Bar | Only progress bar, no other info |
|
||||
|
||||
Configure at **Settings → Display → Status Bar**.
|
||||
|
||||
### Sleep Screen Cover Filter
|
||||
|
||||
When using book cover as sleep screen:
|
||||
|
||||
| Filter | Effect |
|
||||
|--------|--------|
|
||||
| None | Grayscale image as-is |
|
||||
| Contrast | Black and white only (no grays) |
|
||||
| Inverted | Inverted black and white |
|
||||
|
||||
Configure at **Settings → Display → Sleep Screen Cover Filter**.
|
||||
|
||||
---
|
||||
|
||||
## Web Server Features
|
||||
|
||||
The web server provides extended file management and companion app support.
|
||||
|
||||
### Starting the Web Server
|
||||
|
||||
1. Go to **Home → File Transfer**
|
||||
2. Select a WiFi network or create a hotspot
|
||||
3. The web server URL will be displayed
|
||||
|
||||
### File Management
|
||||
|
||||
Access the file manager at `http://<device-ip>/files`
|
||||
|
||||
**Available Operations:**
|
||||
- **Upload** - Upload files via drag-and-drop or file picker
|
||||
- **Download** - Download files to your computer
|
||||
- **Delete** - Remove files and folders
|
||||
- **Rename** - Rename files and folders
|
||||
- **Create Folder** - Create new directories
|
||||
- **Archive/Unarchive** - Archive books (preserves reading progress)
|
||||
- **Copy/Move** - Copy or move files and folders
|
||||
|
||||
### API Access
|
||||
|
||||
The web server provides a JSON API for programmatic access:
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /api/status` | Device status |
|
||||
| `GET /api/files?path=/` | List files |
|
||||
| `GET /api/archived` | List archived books |
|
||||
| `GET /api/hash?path=/book.epub` | Get MD5 hash |
|
||||
|
||||
### mDNS Discovery
|
||||
|
||||
The device advertises itself as `crosspoint.local` on your network.
|
||||
|
||||
### Companion App Support
|
||||
|
||||
The web server supports the CrossPoint Companion Android app:
|
||||
|
||||
1. **QR Code** - Scan the QR code displayed on the web server screen
|
||||
2. **Deep Links** - URLs like `crosspoint://files?host=192.168.1.100` open the app directly
|
||||
|
||||
### Managing Reading Lists via API
|
||||
|
||||
**Get all lists:**
|
||||
```
|
||||
GET /list
|
||||
```
|
||||
|
||||
**Get specific list:**
|
||||
```
|
||||
GET /list?name=MyList
|
||||
```
|
||||
|
||||
**Upload a list:**
|
||||
```
|
||||
POST /list?action=upload&name=MyList
|
||||
Content-Type: text/plain
|
||||
|
||||
1,Book Title,Author Name,/path/to/book.epub
|
||||
2,Another Book,Another Author,/path/to/another.epub
|
||||
```
|
||||
|
||||
**Delete a list:**
|
||||
```
|
||||
POST /list?action=delete&name=MyList
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Fonts
|
||||
|
||||
Two additional accessibility-focused fonts are available.
|
||||
|
||||
### Available Custom Fonts
|
||||
|
||||
1. **Atkinson Hyperlegible Next** - Designed for low-vision readers with high character differentiation
|
||||
2. **Fern Micro** - Optimized for small screens
|
||||
|
||||
### Enabling Custom Fonts
|
||||
|
||||
1. Go to **Settings → Reader → Font Family**
|
||||
2. Select **Custom**
|
||||
3. Go to **Settings → Reader → Custom Font**
|
||||
4. Select your preferred font
|
||||
|
||||
### Fallback Font
|
||||
|
||||
When using custom fonts, set a fallback for missing glyphs:
|
||||
|
||||
1. Go to **Settings → Reader → Fallback Font**
|
||||
2. Choose **Bookerly** or **Noto Sans**
|
||||
|
||||
---
|
||||
|
||||
## Additional Settings
|
||||
|
||||
### Short Power Button Actions
|
||||
|
||||
Configure what happens when you briefly press the Power button:
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| Ignore | No action (default) |
|
||||
| Sleep | Put device to sleep |
|
||||
| Page Turn | Turn to next page |
|
||||
| Dictionary | Open dictionary |
|
||||
| Quick Menu | Open quick menu |
|
||||
|
||||
Configure at **Settings → Controls → Short Power Button Click**.
|
||||
|
||||
### Long-press Chapter Skip
|
||||
|
||||
Control side button long-press behavior:
|
||||
|
||||
- **On** (default) - Long-press Volume buttons to skip chapters
|
||||
- **Off** - Long-press scrolls a page instead
|
||||
|
||||
Configure at **Settings → Controls → Long-press Chapter Skip**.
|
||||
|
||||
### Hyphenation
|
||||
|
||||
Enable word hyphenation for justified text:
|
||||
|
||||
1. Go to **Settings → Reader → Hyphenation**
|
||||
2. Set to **On**
|
||||
|
||||
Hyphenation patterns are available for multiple languages (English, German, French, Spanish, Russian, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Recents View Enhancements
|
||||
|
||||
### Badges
|
||||
|
||||
Books in the Recent tab display badges showing:
|
||||
- **File extension** (epub, txt, md)
|
||||
- **Suffix tags** (X4, X4P for files with `-x4` or `-x4p` suffixes)
|
||||
|
||||
### Removing from Recents
|
||||
|
||||
1. Navigate to a book in the Recent tab
|
||||
2. **Long-press Confirm**
|
||||
3. Select **Remove from Recents**
|
||||
|
||||
### Clearing All Recents
|
||||
|
||||
1. Navigate to any book in the Recent tab
|
||||
2. **Long-press Confirm**
|
||||
3. Select **Clear All Recents**
|
||||
4. Confirm the action
|
||||
|
||||
---
|
||||
|
||||
## Tab Navigation
|
||||
|
||||
The library uses a unified tab bar for navigation.
|
||||
|
||||
### Tabs Available
|
||||
|
||||
| Tab | Contents |
|
||||
|-----|----------|
|
||||
| Recent | Recently opened books |
|
||||
| Lists | Custom reading lists |
|
||||
| Bookmarks | Books with bookmarks |
|
||||
| Search | Search all books |
|
||||
| Files | File browser |
|
||||
|
||||
### Navigating Tabs
|
||||
|
||||
When the tab bar is focused:
|
||||
- **Left/Right** - Switch between tabs
|
||||
- **Down** - Enter the selected tab's content
|
||||
- **Confirm** - Same as Down
|
||||
|
||||
### Tab Overflow
|
||||
|
||||
When tabs don't fit on screen:
|
||||
- **<** indicator appears on left when more tabs exist to the left
|
||||
- **>** indicator appears on right when more tabs exist to the right
|
||||
- Scroll continues automatically when navigating past visible tabs
|
||||
|
||||
---
|
||||
|
||||
## Inline Images
|
||||
|
||||
EPUBs with embedded images now display them inline with text.
|
||||
|
||||
### Supported Formats
|
||||
|
||||
- JPEG (.jpg, .jpeg)
|
||||
- PNG (.png)
|
||||
|
||||
### Image Display
|
||||
|
||||
- Images are automatically scaled to fit the page width
|
||||
- Images are converted to 4-level grayscale with dithering
|
||||
- First load may be slower as images are processed
|
||||
- Subsequent loads use cached versions
|
||||
|
||||
### Image Cache
|
||||
|
||||
Processed images are cached as `.pxc` files in the book's cache directory for faster loading.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Dictionary Not Working
|
||||
|
||||
1. Verify dictionary files are in `/dictionaries/dict-data/`
|
||||
2. Check that all required files exist (.ifo, .idx, .dict.dz)
|
||||
3. File names must match exactly (case-sensitive)
|
||||
|
||||
### Bookmarks Not Saving
|
||||
|
||||
1. Ensure SD card is not write-protected
|
||||
2. Check available storage space
|
||||
3. Bookmarks are saved per-book in `/.crosspoint/`
|
||||
|
||||
### Search Not Finding Books
|
||||
|
||||
1. Search only indexes books in the library
|
||||
2. Ensure books have proper EPUB metadata
|
||||
3. Try searching by filename if metadata is missing
|
||||
|
||||
### Images Not Displaying
|
||||
|
||||
1. Only PNG and JPEG formats are supported
|
||||
2. Very large images may fail to load due to memory constraints
|
||||
3. Check for sufficient free memory (multiple large books open may exhaust memory)
|
||||
|
||||
### Web Server Connection Issues
|
||||
|
||||
1. Ensure device and computer are on the same network
|
||||
2. Try accessing via IP address instead of `crosspoint.local`
|
||||
3. Check that firewall isn't blocking port 80
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts Summary
|
||||
|
||||
### In Reader
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
| Left/Volume Up | Previous page |
|
||||
| Right/Volume Down | Next page |
|
||||
| Left (hold) | Previous chapter |
|
||||
| Right (hold) | Next chapter |
|
||||
| Back | Return to library |
|
||||
| Back (hold) | Return to home |
|
||||
| Confirm | Open chapter selection |
|
||||
| Power (brief) | Configured action (Quick Menu/Dictionary/Sleep/Page Turn) |
|
||||
|
||||
### In Quick Menu
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
| Up/Down/Left/Right | Navigate options |
|
||||
| Confirm | Select option |
|
||||
| Back | Close menu |
|
||||
|
||||
### In Word Selection
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
| Left/Right | Move between words |
|
||||
| Up/Down | Move between lines |
|
||||
| Confirm | Look up word |
|
||||
| Back | Cancel |
|
||||
|
||||
### In Library Tabs
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
| Left/Right | Switch tabs (when tab bar focused) |
|
||||
| Up/Down | Navigate within tab |
|
||||
| Confirm | Select item / Enter tab |
|
||||
| Confirm (hold) | Action menu |
|
||||
| Back | Go back / Exit to home |
|
||||
@ -3,11 +3,40 @@
|
||||
This document show most common issues and possible solutions while using the device features.
|
||||
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Images Not Displaying in EPUBs](#images-not-displaying-in-epubs)
|
||||
- [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network)
|
||||
- [Connection Drops or Times Out](#connection-drops-or-times-out)
|
||||
- [Upload Fails](#upload-fails)
|
||||
- [Saved Password Not Working](#saved-password-not-working)
|
||||
|
||||
### Images Not Displaying in EPUBs
|
||||
|
||||
**Problem:** Some images in EPUB books show as placeholders like "[Image: filename.jpg]" instead of the actual image
|
||||
|
||||
**Possible Causes:**
|
||||
|
||||
1. **Progressive JPEGs are not supported**
|
||||
- The device uses a minimal JPEG decoder optimized for embedded systems
|
||||
- Progressive/multi-scan JPEGs cannot be decoded due to memory constraints
|
||||
- This affects some professionally published EPUBs, especially maps and high-quality photos
|
||||
- **Workaround:** Use Calibre or another EPUB editor to convert progressive JPEGs to baseline JPEGs
|
||||
|
||||
2. **Unsupported image format**
|
||||
- Only JPEG and PNG images are supported
|
||||
- Other formats (GIF, WebP, SVG graphics) will show placeholders
|
||||
|
||||
3. **Image extraction failed**
|
||||
- The image file may be corrupted or the EPUB structure malformed
|
||||
- Try re-downloading the EPUB or converting it with Calibre
|
||||
|
||||
**How to check if an image is progressive JPEG:**
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
print(Image.open("image.jpg").info.get('progressive', 0))
|
||||
# Output: 1 = progressive (not supported), 0 = baseline (supported)
|
||||
```
|
||||
|
||||
### Cannot See the Device on the Network
|
||||
|
||||
**Problem:** Browser shows "Cannot connect" or "Site can't be reached"
|
||||
|
||||
334
docs/webserver-api-reference.md
Normal file
334
docs/webserver-api-reference.md
Normal file
@ -0,0 +1,334 @@
|
||||
# CrossPointWebServer API Reference
|
||||
|
||||
Source: `src/network/CrossPointWebServer.cpp` and `CrossPointWebServer.h`
|
||||
|
||||
## Server Configuration
|
||||
|
||||
- HTTP port: 80 (default)
|
||||
- WebSocket port: 81 (default)
|
||||
- WiFi sleep disabled for responsiveness
|
||||
- Supports both STA (station) and AP (access point) modes
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
### GET /
|
||||
**Handler:** `handleRoot()`
|
||||
**Response:** HTML homepage from `HomePageHtml` (generated from `html/HomePage.html`)
|
||||
**Content-Type:** text/html
|
||||
|
||||
### GET /files
|
||||
**Handler:** `handleFileList()`
|
||||
**Response:** HTML file browser page from `FilesPageHtml` (generated from `html/FilesPage.html`)
|
||||
**Content-Type:** text/html
|
||||
|
||||
### GET /api/status
|
||||
**Handler:** `handleStatus()`
|
||||
**Response:** JSON device status
|
||||
**Content-Type:** application/json
|
||||
```json
|
||||
{
|
||||
"version": "CROSSPOINT_VERSION",
|
||||
"ip": "192.168.x.x",
|
||||
"mode": "AP" | "STA",
|
||||
"rssi": -50, // 0 in AP mode
|
||||
"freeHeap": 123456,
|
||||
"uptime": 3600 // seconds
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/files
|
||||
**Handler:** `handleFileListData()`
|
||||
**Query params:**
|
||||
- `path` (optional): Directory path, defaults to "/"
|
||||
- `showHidden` (optional): "true" to show dot-files (except .crosspoint)
|
||||
**Response:** JSON array of files
|
||||
**Content-Type:** application/json
|
||||
```json
|
||||
[
|
||||
{"name": "book.epub", "size": 123456, "isDirectory": false, "isEpub": true},
|
||||
{"name": "folder", "size": 0, "isDirectory": true, "isEpub": false}
|
||||
]
|
||||
```
|
||||
**Notes:**
|
||||
- Hidden by default: files starting with ".", "System Volume Information", "XTCache"
|
||||
- Always hidden: ".crosspoint" (internal cache folder)
|
||||
- Streamed response (chunked encoding) to reduce memory usage
|
||||
|
||||
### GET /api/archived
|
||||
**Handler:** `handleArchivedList()`
|
||||
**Response:** JSON array of archived books
|
||||
**Content-Type:** application/json
|
||||
```json
|
||||
[
|
||||
{"filename": "archived_file.epub", "originalPath": "/Books/archived_file.epub"}
|
||||
]
|
||||
```
|
||||
**Notes:** Uses `BookManager::listArchivedBooks()` and `BookManager::getArchivedBookOriginalPath()`
|
||||
|
||||
### GET /download
|
||||
**Handler:** `handleDownload()`
|
||||
**Query params:**
|
||||
- `path` (required): File path to download
|
||||
**Response:** File binary with Content-Disposition attachment header
|
||||
**Content-Type:** application/octet-stream
|
||||
**Errors:**
|
||||
- 400: Missing path, path is directory
|
||||
- 403: Hidden/system file, protected item
|
||||
- 404: File not found
|
||||
**Notes:**
|
||||
- Streams in 4KB chunks
|
||||
- Updates `totalBytesDownloaded` and `totalFilesDownloaded` stats
|
||||
- Security: rejects paths with "..", files starting with ".", protected items
|
||||
|
||||
### POST /upload
|
||||
**Handler:** `handleUpload()` (multipart handler), `handleUploadPost()` (response handler)
|
||||
**Query params:**
|
||||
- `path` (optional): Upload directory, defaults to "/"
|
||||
**Form data:** multipart/form-data with file
|
||||
**Response:** "File uploaded successfully: filename" or error message
|
||||
**Notes:**
|
||||
- Uses 4KB write buffer for SD card efficiency
|
||||
- Overwrites existing files
|
||||
- Clears epub cache after upload via `clearEpubCacheIfNeeded()`
|
||||
- Updates `totalBytesUploaded` and `totalFilesUploaded` stats
|
||||
- Logs progress every 100KB
|
||||
|
||||
### POST /mkdir
|
||||
**Handler:** `handleCreateFolder()`
|
||||
**Form params:**
|
||||
- `name` (required): Folder name
|
||||
- `path` (optional): Parent directory, defaults to "/"
|
||||
**Response:** "Folder created: foldername" or error
|
||||
**Errors:**
|
||||
- 400: Missing name, empty name, folder exists
|
||||
|
||||
### POST /delete
|
||||
**Handler:** `handleDelete()`
|
||||
**Form params:**
|
||||
- `path` (required): Item path to delete
|
||||
- `type` (optional): "file" (default) or "folder"
|
||||
- `archived` (optional): "true" for archived books
|
||||
**Response:** "Deleted successfully" or error
|
||||
**Errors:**
|
||||
- 400: Missing path, root directory, folder not empty
|
||||
- 403: Hidden/system file, protected item
|
||||
- 404: Item not found
|
||||
- 500: Delete failed
|
||||
**Notes:**
|
||||
- For files: uses `BookManager::deleteBook()` which handles cache and recent books cleanup
|
||||
- For folders: must be empty first
|
||||
- For archived: passes filename to `BookManager::deleteBook(filename, true)`
|
||||
|
||||
### POST /archive
|
||||
**Handler:** `handleArchive()`
|
||||
**Form params:**
|
||||
- `path` (required): Book path to archive
|
||||
**Response:** "Book archived successfully" or error
|
||||
**Notes:** Uses `BookManager::archiveBook()`
|
||||
|
||||
### POST /unarchive
|
||||
**Handler:** `handleUnarchive()`
|
||||
**Form params:**
|
||||
- `filename` (required): Archived book filename
|
||||
**Response:** JSON with original path
|
||||
**Content-Type:** application/json
|
||||
```json
|
||||
{"success": true, "originalPath": "/Books/book.epub"}
|
||||
```
|
||||
**Notes:** Uses `BookManager::unarchiveBook()` and `BookManager::getArchivedBookOriginalPath()`
|
||||
|
||||
### POST /rename
|
||||
**Handler:** `handleRename()`
|
||||
**Form params:**
|
||||
- `path` (required): Current item path
|
||||
- `newName` (required): New name (filename only, no path separators)
|
||||
**Response:** "Renamed successfully" or error
|
||||
**Errors:**
|
||||
- 400: Missing params, empty name, name contains "/" or "\\", root directory, destination exists
|
||||
- 403: System file, protected item
|
||||
- 404: Source not found
|
||||
- 500: Rename failed
|
||||
**Notes:**
|
||||
- Renames in place (same directory, new name)
|
||||
- Uses `SdMan.rename()`
|
||||
- Clears epub cache after rename via `clearEpubCacheIfNeeded()`
|
||||
|
||||
### POST /copy
|
||||
**Handler:** `handleCopy()`
|
||||
**Form params:**
|
||||
- `srcPath` (required): Source path
|
||||
- `destPath` (required): Full destination path (including new name)
|
||||
**Response:** "Copied successfully" or error
|
||||
**Errors:**
|
||||
- 400: Missing params, root directory, destination exists, copy into self
|
||||
- 403: System file, protected item
|
||||
- 404: Source not found
|
||||
- 500: Copy failed
|
||||
**Notes:**
|
||||
- Uses `copyFile()` for files (4KB buffer chunks)
|
||||
- Uses `copyFolder()` for recursive directory copy
|
||||
- Skips hidden files in folder copy
|
||||
|
||||
### POST /move
|
||||
**Handler:** `handleMove()`
|
||||
**Form params:**
|
||||
- `srcPath` (required): Source path
|
||||
- `destPath` (required): Full destination path (including new name)
|
||||
**Response:** "Moved successfully" or error
|
||||
**Errors:** Same as copy
|
||||
**Notes:**
|
||||
- First attempts atomic `SdMan.rename()` (fast)
|
||||
- Falls back to copy+delete if rename fails
|
||||
- Uses `deleteFolderRecursive()` for folder cleanup
|
||||
|
||||
### GET /list
|
||||
**Handler:** `handleListGet()`
|
||||
**Query params:**
|
||||
- `name` (optional): Specific list name to retrieve
|
||||
**Response:** JSON array of lists (if no name) or single list details (if name specified)
|
||||
**Content-Type:** application/json
|
||||
|
||||
**Response (all lists - no name param):**
|
||||
```json
|
||||
[
|
||||
{"name": "MyReadingList", "path": "/.lists/MyReadingList.bin", "bookCount": 5},
|
||||
{"name": "Favorites", "path": "/.lists/Favorites.bin", "bookCount": 12}
|
||||
]
|
||||
```
|
||||
|
||||
**Response (specific list - with name param):**
|
||||
```json
|
||||
{
|
||||
"name": "MyReadingList",
|
||||
"path": "/.lists/MyReadingList.bin",
|
||||
"books": [
|
||||
{"order": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "path": "/Books/gatsby.epub"},
|
||||
{"order": 2, "title": "1984", "author": "George Orwell", "path": "/Books/1984.epub"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 404: List not found (when `name` specified but doesn't exist)
|
||||
|
||||
**Notes:**
|
||||
- Lists are stored in `/.lists/` directory as `.bin` files
|
||||
- Uses `BookListStore::listAllLists()` and `BookListStore::loadList()`
|
||||
|
||||
### POST /list
|
||||
**Handler:** `handleListPost()`
|
||||
**Query params:**
|
||||
- `action` (required): "upload" or "delete"
|
||||
- `name` (required): List name (without .bin extension)
|
||||
**Request body (for upload):** CSV text with one book per line
|
||||
**Content-Type:** text/plain (for upload body)
|
||||
|
||||
**Input format (POST body for upload action):**
|
||||
```
|
||||
1,The Great Gatsby,F. Scott Fitzgerald,/Books/gatsby.epub
|
||||
2,1984,George Orwell,/Books/1984.epub
|
||||
3,Pride and Prejudice,Jane Austen,/Books/pride.epub
|
||||
```
|
||||
Format: `order,title,author,path` (one per line)
|
||||
|
||||
**Response (upload success):**
|
||||
```json
|
||||
{"success": true, "path": "/.lists/MyReadingList.bin"}
|
||||
```
|
||||
|
||||
**Response (delete success):**
|
||||
```json
|
||||
{"success": true}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 400: Missing action or name parameter, empty name, failed to parse list data
|
||||
- 404: List not found (for delete action)
|
||||
- 500: Failed to save/delete list
|
||||
|
||||
**Notes:**
|
||||
- Lists are stored as binary files in `/.lists/` directory
|
||||
- Uses `BookListStore::parseFromText()`, `BookListStore::saveList()`, `BookListStore::deleteList()`
|
||||
- Order field determines display order (books are sorted by order when loaded)
|
||||
- Overwrites existing list with same name on upload
|
||||
|
||||
## WebSocket Protocol (port 81)
|
||||
|
||||
**Handler:** `onWebSocketEvent()` via `wsEventCallback()` trampoline
|
||||
|
||||
### Upload Protocol
|
||||
|
||||
1. Client connects
|
||||
2. Server: (implicit connection acknowledgment)
|
||||
3. Client TEXT: `START:<filename>:<size>:<path>`
|
||||
4. Server TEXT: `READY` or `ERROR:<message>`
|
||||
5. Client BIN: file data chunks (any size, recommend 64KB)
|
||||
6. Server TEXT: `PROGRESS:<received>:<total>` (every 64KB or at end)
|
||||
7. Server TEXT: `DONE` or `ERROR:<message>`
|
||||
|
||||
### Events
|
||||
- `WStype_CONNECTED`: Client connected, logs connection
|
||||
- `WStype_DISCONNECTED`: Cleanup incomplete upload, delete partial file
|
||||
- `WStype_TEXT`: Parse control messages (START)
|
||||
- `WStype_BIN`: Write file data, send progress, complete upload
|
||||
|
||||
### Notes
|
||||
- Faster than HTTP multipart for large files
|
||||
- Direct binary writes to SD card
|
||||
- Clears epub cache after upload
|
||||
- Updates traffic statistics
|
||||
|
||||
## Security
|
||||
|
||||
### Protected Items (HIDDEN_ITEMS[])
|
||||
- "System Volume Information"
|
||||
- "XTCache"
|
||||
|
||||
### Always Hidden
|
||||
- ".crosspoint" (internal cache)
|
||||
|
||||
### Security Checks Applied To
|
||||
- `/delete`: Rejects dot-files (unless archived), protected items
|
||||
- `/download`: Rejects dot-files, protected items, path traversal (..)
|
||||
- `/rename`: Rejects dot-files, protected items
|
||||
- `/copy`: Rejects dot-files, protected items
|
||||
- `/move`: Rejects dot-files, protected items
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### clearEpubCacheIfNeeded(filePath)
|
||||
- Location: anonymous namespace at top of file
|
||||
- Clears epub cache if file ends with ".epub"
|
||||
- Uses `Epub(filePath, "/.crosspoint").clearCache()`
|
||||
- Called by: upload, WebSocket upload, rename
|
||||
|
||||
### scanFiles(path, callback, showHidden)
|
||||
- Iterates directory, calls callback for each FileInfo
|
||||
- Yields and resets watchdog during iteration
|
||||
- Filters hidden items based on showHidden flag
|
||||
|
||||
### copyFile(srcPath, destPath) / copyFolder(srcPath, destPath)
|
||||
- 4KB buffer for file copy
|
||||
- Recursive for folders
|
||||
- Returns bool success
|
||||
|
||||
### deleteFolderRecursive(path)
|
||||
- Static helper for move fallback
|
||||
- Recursively deletes contents then directory
|
||||
|
||||
## Traffic Statistics (mutable, updated from const handlers)
|
||||
- `totalBytesUploaded`
|
||||
- `totalBytesDownloaded`
|
||||
- `totalFilesUploaded`
|
||||
- `totalFilesDownloaded`
|
||||
- `serverStartTime` (for uptime calculation)
|
||||
|
||||
## Dependencies
|
||||
- `<WebServer.h>` - ESP32 HTTP server
|
||||
- `<WebSocketsServer.h>` - WebSocket support
|
||||
- `<ArduinoJson.h>` - JSON serialization
|
||||
- `<SDCardManager.h>` - SD card operations (SdMan singleton)
|
||||
- `<Epub.h>` - Epub cache management
|
||||
- `BookListStore.h` - Book list management (lists feature)
|
||||
- `BookManager.h` - Book deletion, archiving, recent books
|
||||
- `StringUtils.h` - File extension checking
|
||||
195
ef-CHANGELOG.md
Normal file
195
ef-CHANGELOG.md
Normal file
@ -0,0 +1,195 @@
|
||||
# crosspoint-ef Changelog
|
||||
|
||||
All notable changes to the crosspoint-ef fork are documented here.
|
||||
|
||||
Base: CrossPoint Reader 0.15.0
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.5
|
||||
|
||||
**Stability & Memory Improvements**
|
||||
|
||||
### Bug Fixes - Webserver
|
||||
|
||||
- **File Transfer Stability**: Removed blocking MD5 hash computation from file listings that caused EAGAIN errors and connection stalls
|
||||
- **JSON Batching**: Implemented 2KB batch streaming for file listings with pacing to prevent TCP buffer overflow
|
||||
- **Simplified Flow Control**: Removed unnecessary yield/delay logic from content streaming
|
||||
|
||||
### Bug Fixes - Memory
|
||||
|
||||
- **QR Code Caching**: Generate QR codes once on server start instead of regenerating on each screen render
|
||||
- **WiFi Scan Optimization**: Replaced memory-heavy `std::map` deduplication with in-place vector search, limited results to 20 networks, earlier `WiFi.scanDelete()` for faster memory recovery
|
||||
- **Cover Buffer Leak**: Fixed 48KB memory leak when navigating from Home to File Transfer (cover buffer now explicitly freed)
|
||||
|
||||
### Bug Fixes - EPUB Reader
|
||||
|
||||
- **Errant Underlining**: Fixed words before styled inline elements (like `<a>` tags with CSS underline) incorrectly receiving the element's style by flushing the text buffer before style changes
|
||||
|
||||
### Bug Fixes - Flashing Screen
|
||||
|
||||
- **Version String Overflow**: Fixed flash notification parsing failing on longer version strings (buffer limit increased from 30 to 50 characters)
|
||||
- **Display Quality**: Changed flashing screen to half refresh for cleaner appearance
|
||||
- **Timing**: Adjusted pre-flash script timing for half refresh completion
|
||||
|
||||
### Upstream Merges
|
||||
|
||||
- **PR #522 - HAL Abstraction Layer**: Merged hardware abstraction layer refactor introducing `HalDisplay` and `HalGPIO` classes, decoupling application code from direct hardware access
|
||||
- **PR #603 - Sunlight Fading Fix**: Added user-toggleable setting to turn off display between refreshes, mitigating the sunlight fading issue on e-ink displays
|
||||
- New "Sunlight Fading Fix" toggle in Display settings (OFF/ON)
|
||||
- Passes `turnOffScreen` parameter through display stack when enabled
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `src/main.cpp` - flash screen fixes, cover buffer free on File Transfer entry, fading fix integration
|
||||
- `scripts/pre_flash.py` - timing adjustments for full refresh
|
||||
- `src/network/CrossPointWebServer.cpp` - JSON batching, removed MD5 from listings
|
||||
- `src/network/CrossPointWebServer.h` - removed md5 from FileInfo, simplified sendContentSafe
|
||||
- `src/activities/network/CrossPointWebServerActivity.cpp` - QR code caching
|
||||
- `src/activities/network/CrossPointWebServerActivity.h` - QR code cache members
|
||||
- `src/activities/network/WifiSelectionActivity.cpp` - WiFi scan memory optimization
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` - flush buffer before style changes
|
||||
- `lib/hal/HalDisplay.h` - new HAL abstraction for display (PR #522), turnOffScreen parameter (PR #603)
|
||||
- `lib/hal/HalDisplay.cpp` - HAL display implementation with fading fix passthrough
|
||||
- `lib/hal/HalGPIO.h` - new HAL abstraction for GPIO (PR #522)
|
||||
- `lib/hal/HalGPIO.cpp` - HAL GPIO implementation
|
||||
- `lib/GfxRenderer/GfxRenderer.h` - updated for HAL layer, added fadingFix member
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` - updated for HAL layer, passes fadingFix to display
|
||||
- `src/CrossPointSettings.h` - added fadingFix setting
|
||||
- `src/CrossPointSettings.cpp` - fadingFix persistence
|
||||
- `src/activities/settings/SettingsActivity.cpp` - added Sunlight Fading Fix toggle
|
||||
- `open-x4-sdk` - updated submodule with turnOffScreen support in EInkDisplay
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.4
|
||||
|
||||
**EPUB Rendering & Stability**
|
||||
|
||||
### New Features
|
||||
|
||||
- **End-of-Book "Start Over"**: Press next at end of book to wrap to first page
|
||||
|
||||
### EPUB Rendering Improvements
|
||||
|
||||
- CSS `margin-left`/`padding-left` parsing for block indentation
|
||||
- Vertical bar and italic styling for blockquotes
|
||||
- Left margin indentation for list items (`<ol>`/`<ul>`)
|
||||
- Fixed ordered lists showing bullets instead of numbers
|
||||
- Fixed nested `<p>` inside `<li>` causing marker on separate line
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Webserver**: Fixed file listing disconnection issues with flow control
|
||||
- **Webserver**: Memory optimization for File Transfer mode (frees heap before starting)
|
||||
- **Dictionary**: Fixed zip dictionary allocation order for better memory allocation success
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.3
|
||||
|
||||
**Maintenance Release**
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed cppcheck CI failure: removed unused `screenWidth` variable in word selection activity
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.2
|
||||
|
||||
**Quick Menu Enhancements**
|
||||
|
||||
### New Features
|
||||
|
||||
- **Screen Rotation Toggle**: Quick toggle between Portrait and Landscape CCW directly from the quick menu
|
||||
- Automatically reindexes content for new screen dimensions
|
||||
- Preserves reading position via content offset restoration
|
||||
- **Customizable Menu Order**: Reorder quick menu items to your preference
|
||||
- New "Edit List Order" option at bottom of menu
|
||||
- Pick-and-place reordering: select item, navigate to destination, place
|
||||
- Order persists across sessions
|
||||
|
||||
### UI Improvements
|
||||
|
||||
- Added navigation button hints to quick menu (prev/next on front buttons, up/down on side buttons)
|
||||
- Fixed orientation-aware margins for button hint areas in landscape modes
|
||||
- New default menu order: Bookmark, Dictionary, Rotate Screen, Settings, Clear Cache
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.1
|
||||
|
||||
**Dictionary Stability & UX Improvements**
|
||||
|
||||
### Bug Fixes - Stability
|
||||
|
||||
- Fixed dictionary crashes caused by heap fragmentation from repeated page navigation
|
||||
- Refactored TextBlock/ParsedText from `std::list` to `std::vector`, reducing heap allocations by ~12x per TextBlock
|
||||
- Affects EPUB reader page rendering, dictionary definition display, and word selection
|
||||
- Contiguous memory improves cache locality during text layout and reduces heap fragmentation on the memory-constrained ESP32
|
||||
- Added uncompressed dictionary (`.dict`) support to avoid decompression memory issues with large dictzip chunks (58KB chunks -> direct read)
|
||||
- Implemented chunked on-demand HTML parsing for large definitions, parsing pages as user navigates rather than all at once
|
||||
- Limited cached pages to 4 with re-parse capability for backward navigation beyond cache window
|
||||
- Fixed double-button press bug when loading new dictionary chunks
|
||||
|
||||
### Bug Fixes - UI/Layout
|
||||
|
||||
- Restored proper orientation-aware button hint spacing (front: 45px, side: 50px)
|
||||
- Added side button hints to definition screen with "<" / ">" labels for page navigation
|
||||
- Added side button hints to word selection screen ("UP"/"DOWN" labels, borderless, small font)
|
||||
- Added side button hints to dictionary menu ("< Prev", "Next >")
|
||||
- Moved page indicator up to avoid bezel cutoff in landscape orientations
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.0
|
||||
|
||||
**First Official Release** (previously ef-0.15.99)
|
||||
|
||||
First milestone release of the crosspoint-ef fork, building on CrossPoint Reader 0.15.0 with 14+ major new features and enhancements.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Dictionary Support**: Offline StarDict dictionary with word selection from reader, fast prefix-indexed search, rich HTML formatting, and multi-page pagination
|
||||
- **Bookmark System**: Per-book bookmarks with visual folded-corner indicators, dedicated management interface, and auto-generated bookmark names
|
||||
- **Quick Menu**: In-reader quick access menu for common actions (Dictionary, Bookmark, Clear Cache, Settings) via short power button press
|
||||
- **Library Search**: Search across all books by title, author, or filename with dynamic character picker and weighted relevance scoring
|
||||
- **CSS Support**: Parse and apply CSS styles from EPUB stylesheets (text-align, font-style, font-weight, text-decoration, margins, padding)
|
||||
- **Inline Image Support**: PNG and Baseline JPEG rendering within EPUB content with 2-bit grayscale dithering and caching
|
||||
- **Custom Fonts**: Atkinson Hyperlegible Next (low-vision readers) and Fern Micro (small screens)
|
||||
- **Enhanced Web Server**: File management (upload, download, delete, rename, copy, move, mkdir), companion app API, WebSocket uploads, mDNS discovery at `crosspoint.local`
|
||||
- **Reading Lists**: Create, manage, and pin custom book lists with web API support (CSV format)
|
||||
- **Enhanced Tab Bar**: Unified tab bar with horizontal scrolling and overflow indicators (Recent, Lists, Bookmarks, Search, Files)
|
||||
- **Progress Bar Status**: Additional status bar option showing visual reading progress
|
||||
- **OPDS Browser Enhancements**: Navigation history, page skipping (hold Up/Down), error retry, HTTP Basic Auth support
|
||||
|
||||
### Display Enhancements
|
||||
|
||||
- **High Contrast Mode**: System-wide contrast adjustment
|
||||
- **Bezel Compensation**: Configurable margin (0-10px) for physical screen edge defects
|
||||
- **Sleep Screen Improvements**: Edge-aware color filling for seamless letterbox appearance
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed device hanging when booted without USB connected (Serial.available()/Serial.read() called without Serial.begin())
|
||||
- Fixed grayscale state corruption causing ghosting artifacts when anti-aliasing enabled under memory pressure
|
||||
- Memory optimization with graceful degradation when memory is low
|
||||
|
||||
### Development Tools
|
||||
|
||||
- `pre_flash.py`: Displays "Flashing firmware..." screen during upload
|
||||
- `debugging_monitor.py`: Enhanced serial monitor with memory graphs
|
||||
- `pio_helper.py`: Interactive PlatformIO workflow helper
|
||||
|
||||
---
|
||||
|
||||
## Differences from Upstream 0.16.0
|
||||
|
||||
This fork is based on upstream 0.15.0. The following 0.16.0 features are not included:
|
||||
|
||||
- KOReader sync support
|
||||
- Non-English hyphenation patterns (Spanish, German, French, Russian)
|
||||
- XTC/XTCH file format support
|
||||
|
||||
See [crosspoint-ef-features.md](docs/crosspoint-ef-features.md) for complete feature documentation.
|
||||
@ -33,23 +33,10 @@
|
||||
#include <builtinFonts/notosans_18_bolditalic.h>
|
||||
#include <builtinFonts/notosans_18_italic.h>
|
||||
#include <builtinFonts/notosans_18_regular.h>
|
||||
#include <builtinFonts/opendyslexic_10_bold.h>
|
||||
#include <builtinFonts/opendyslexic_10_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_10_italic.h>
|
||||
#include <builtinFonts/opendyslexic_10_regular.h>
|
||||
#include <builtinFonts/opendyslexic_12_bold.h>
|
||||
#include <builtinFonts/opendyslexic_12_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_12_italic.h>
|
||||
#include <builtinFonts/opendyslexic_12_regular.h>
|
||||
#include <builtinFonts/opendyslexic_14_bold.h>
|
||||
#include <builtinFonts/opendyslexic_14_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_14_italic.h>
|
||||
#include <builtinFonts/opendyslexic_14_regular.h>
|
||||
#include <builtinFonts/opendyslexic_8_bold.h>
|
||||
#include <builtinFonts/opendyslexic_8_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_8_italic.h>
|
||||
#include <builtinFonts/opendyslexic_8_regular.h>
|
||||
#include <builtinFonts/ubuntu_10_bold.h>
|
||||
#include <builtinFonts/ubuntu_10_regular.h>
|
||||
#include <builtinFonts/ubuntu_12_bold.h>
|
||||
#include <builtinFonts/ubuntu_12_regular.h>
|
||||
|
||||
// Custom fonts registry (generated by convert-builtin-fonts.sh)
|
||||
#include <builtinFonts/custom/customFonts.h>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Bold.ttf
Normal file
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Italic.ttf
Normal file
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Italic.ttf
Normal file
Binary file not shown.
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Regular.ttf
Normal file
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Regular.ttf
Normal file
Binary file not shown.
1555
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_12_bold.h
Normal file
1555
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_12_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1588
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_12_italic.h
Normal file
1588
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_12_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1986
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_14_bold.h
Normal file
1986
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_14_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2012
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_14_italic.h
Normal file
2012
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_14_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2393
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_16_bold.h
Normal file
2393
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_16_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2427
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_16_italic.h
Normal file
2427
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_16_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2978
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_18_bold.h
Normal file
2978
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_18_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3018
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_18_italic.h
Normal file
3018
lib/EpdFont/builtinFonts/custom/atkinsonhyperlegiblenext_18_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
99
lib/EpdFont/builtinFonts/custom/customFonts.h
Normal file
99
lib/EpdFont/builtinFonts/custom/customFonts.h
Normal file
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Generated by convert-builtin-fonts.sh
|
||||
* Registry of available custom fonts
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <EpdFont.h>
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
#define CUSTOM_FONT_COUNT 2
|
||||
|
||||
static const char* CUSTOM_FONT_NAMES[] = {
|
||||
"AtkinsonHyperlegibleNext",
|
||||
"FernMicro"
|
||||
};
|
||||
|
||||
// Include all custom font headers
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_12_regular.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_12_italic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_12_bold.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_12_bolditalic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_14_regular.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_14_italic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_14_bold.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_14_bolditalic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_16_regular.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_16_italic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_16_bold.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_16_bolditalic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_18_regular.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_18_italic.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_18_bold.h>
|
||||
#include <builtinFonts/custom/atkinsonhyperlegiblenext_18_bolditalic.h>
|
||||
#include <builtinFonts/custom/fernmicro_12_regular.h>
|
||||
#include <builtinFonts/custom/fernmicro_12_italic.h>
|
||||
#include <builtinFonts/custom/fernmicro_12_bold.h>
|
||||
#include <builtinFonts/custom/fernmicro_12_bolditalic.h>
|
||||
#include <builtinFonts/custom/fernmicro_14_regular.h>
|
||||
#include <builtinFonts/custom/fernmicro_14_italic.h>
|
||||
#include <builtinFonts/custom/fernmicro_14_bold.h>
|
||||
#include <builtinFonts/custom/fernmicro_14_bolditalic.h>
|
||||
#include <builtinFonts/custom/fernmicro_16_regular.h>
|
||||
#include <builtinFonts/custom/fernmicro_16_italic.h>
|
||||
#include <builtinFonts/custom/fernmicro_16_bold.h>
|
||||
#include <builtinFonts/custom/fernmicro_16_bolditalic.h>
|
||||
#include <builtinFonts/custom/fernmicro_18_regular.h>
|
||||
#include <builtinFonts/custom/fernmicro_18_italic.h>
|
||||
#include <builtinFonts/custom/fernmicro_18_bold.h>
|
||||
#include <builtinFonts/custom/fernmicro_18_bolditalic.h>
|
||||
|
||||
// Extern EpdFont declarations for custom fonts
|
||||
extern EpdFont atkinsonhyperlegiblenext12RegularFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext12ItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext12BoldFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext12BoldItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext14RegularFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext14ItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext14BoldFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext14BoldItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext16RegularFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext16ItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext16BoldFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext16BoldItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext18RegularFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext18ItalicFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext18BoldFont;
|
||||
extern EpdFont atkinsonhyperlegiblenext18BoldItalicFont;
|
||||
extern EpdFont fernmicro12RegularFont;
|
||||
extern EpdFont fernmicro12ItalicFont;
|
||||
extern EpdFont fernmicro12BoldFont;
|
||||
extern EpdFont fernmicro12BoldItalicFont;
|
||||
extern EpdFont fernmicro14RegularFont;
|
||||
extern EpdFont fernmicro14ItalicFont;
|
||||
extern EpdFont fernmicro14BoldFont;
|
||||
extern EpdFont fernmicro14BoldItalicFont;
|
||||
extern EpdFont fernmicro16RegularFont;
|
||||
extern EpdFont fernmicro16ItalicFont;
|
||||
extern EpdFont fernmicro16BoldFont;
|
||||
extern EpdFont fernmicro16BoldItalicFont;
|
||||
extern EpdFont fernmicro18RegularFont;
|
||||
extern EpdFont fernmicro18ItalicFont;
|
||||
extern EpdFont fernmicro18BoldFont;
|
||||
extern EpdFont fernmicro18BoldItalicFont;
|
||||
|
||||
// Extern EpdFontFamily declarations for custom fonts
|
||||
extern EpdFontFamily atkinsonhyperlegiblenext12FontFamily;
|
||||
extern EpdFontFamily atkinsonhyperlegiblenext14FontFamily;
|
||||
extern EpdFontFamily atkinsonhyperlegiblenext16FontFamily;
|
||||
extern EpdFontFamily atkinsonhyperlegiblenext18FontFamily;
|
||||
extern EpdFontFamily fernmicro12FontFamily;
|
||||
extern EpdFontFamily fernmicro14FontFamily;
|
||||
extern EpdFontFamily fernmicro16FontFamily;
|
||||
extern EpdFontFamily fernmicro18FontFamily;
|
||||
|
||||
// Function to register all custom fonts with the renderer
|
||||
void registerCustomFonts(GfxRenderer& renderer);
|
||||
|
||||
2174
lib/EpdFont/builtinFonts/custom/fernmicro_12_bold.h
Normal file
2174
lib/EpdFont/builtinFonts/custom/fernmicro_12_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
2144
lib/EpdFont/builtinFonts/custom/fernmicro_12_bolditalic.h
Normal file
2144
lib/EpdFont/builtinFonts/custom/fernmicro_12_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
1918
lib/EpdFont/builtinFonts/custom/fernmicro_12_italic.h
Normal file
1918
lib/EpdFont/builtinFonts/custom/fernmicro_12_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
1932
lib/EpdFont/builtinFonts/custom/fernmicro_12_regular.h
Normal file
1932
lib/EpdFont/builtinFonts/custom/fernmicro_12_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
2746
lib/EpdFont/builtinFonts/custom/fernmicro_14_bold.h
Normal file
2746
lib/EpdFont/builtinFonts/custom/fernmicro_14_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
2677
lib/EpdFont/builtinFonts/custom/fernmicro_14_bolditalic.h
Normal file
2677
lib/EpdFont/builtinFonts/custom/fernmicro_14_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
2405
lib/EpdFont/builtinFonts/custom/fernmicro_14_italic.h
Normal file
2405
lib/EpdFont/builtinFonts/custom/fernmicro_14_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
2474
lib/EpdFont/builtinFonts/custom/fernmicro_14_regular.h
Normal file
2474
lib/EpdFont/builtinFonts/custom/fernmicro_14_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
3393
lib/EpdFont/builtinFonts/custom/fernmicro_16_bold.h
Normal file
3393
lib/EpdFont/builtinFonts/custom/fernmicro_16_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
3332
lib/EpdFont/builtinFonts/custom/fernmicro_16_bolditalic.h
Normal file
3332
lib/EpdFont/builtinFonts/custom/fernmicro_16_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
3001
lib/EpdFont/builtinFonts/custom/fernmicro_16_italic.h
Normal file
3001
lib/EpdFont/builtinFonts/custom/fernmicro_16_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
3074
lib/EpdFont/builtinFonts/custom/fernmicro_16_regular.h
Normal file
3074
lib/EpdFont/builtinFonts/custom/fernmicro_16_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
4121
lib/EpdFont/builtinFonts/custom/fernmicro_18_bold.h
Normal file
4121
lib/EpdFont/builtinFonts/custom/fernmicro_18_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
4051
lib/EpdFont/builtinFonts/custom/fernmicro_18_bolditalic.h
Normal file
4051
lib/EpdFont/builtinFonts/custom/fernmicro_18_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
3648
lib/EpdFont/builtinFonts/custom/fernmicro_18_italic.h
Normal file
3648
lib/EpdFont/builtinFonts/custom/fernmicro_18_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
3706
lib/EpdFont/builtinFonts/custom/fernmicro_18_regular.h
Normal file
3706
lib/EpdFont/builtinFonts/custom/fernmicro_18_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -135,3 +135,56 @@ ruby -rdigest -e 'puts [
|
||||
"./notosans_8_regular.h",
|
||||
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||
))"
|
||||
|
||||
# Custom font sizes (must match convert-builtin-fonts.sh)
|
||||
CUSTOM_FONT_SIZES=(12 14 16 18)
|
||||
|
||||
# Generate font IDs for all custom fonts
|
||||
echo ""
|
||||
echo "// Custom font IDs"
|
||||
|
||||
CUSTOM_DIR="./custom"
|
||||
declare -a CUSTOM_FONT_NAMES=()
|
||||
declare -a CUSTOM_FONT_UPPERCASE=()
|
||||
|
||||
if [ -d "$CUSTOM_DIR" ]; then
|
||||
for font_dir in "$CUSTOM_DIR"/*/; do
|
||||
if [ -d "$font_dir" ]; then
|
||||
font_folder_name=$(basename "$font_dir")
|
||||
font_name_lower=$(echo "$font_folder_name" | tr '[:upper:]' '[:lower:]')
|
||||
font_name_upper=$(echo "$font_folder_name" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
# Check if font headers exist (Regular is required)
|
||||
if [ -f "./custom/${font_name_lower}_12_regular.h" ]; then
|
||||
CUSTOM_FONT_NAMES+=("$font_name_lower")
|
||||
CUSTOM_FONT_UPPERCASE+=("$font_name_upper")
|
||||
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
font_id=$(ruby -rdigest -e "puts [
|
||||
\"./custom/${font_name_lower}_${size}_regular.h\",
|
||||
\"./custom/${font_name_lower}_${size}_bold.h\",
|
||||
\"./custom/${font_name_lower}_${size}_bolditalic.h\",
|
||||
\"./custom/${font_name_lower}_${size}_italic.h\",
|
||||
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)")
|
||||
echo "#define ${font_name_upper}_${size}_FONT_ID ($font_id)"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Generate CUSTOM_FONT_IDS lookup array
|
||||
echo ""
|
||||
echo "// Custom font ID lookup array: CUSTOM_FONT_IDS[fontIndex][sizeIndex]"
|
||||
echo "// Size indices: 0=12pt, 1=14pt, 2=16pt, 3=18pt"
|
||||
|
||||
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||
echo "static const int CUSTOM_FONT_IDS[][4] = {"
|
||||
for i in "${!CUSTOM_FONT_UPPERCASE[@]}"; do
|
||||
font_upper="${CUSTOM_FONT_UPPERCASE[$i]}"
|
||||
echo " {${font_upper}_12_FONT_ID, ${font_upper}_14_FONT_ID, ${font_upper}_16_FONT_ID, ${font_upper}_18_FONT_ID},"
|
||||
done
|
||||
echo "};"
|
||||
else
|
||||
echo "static const int CUSTOM_FONT_IDS[][4] = {};"
|
||||
fi
|
||||
|
||||
@ -9,6 +9,9 @@ BOOKERLY_FONT_SIZES=(12 14 16 18)
|
||||
NOTOSANS_FONT_SIZES=(12 14 16 18)
|
||||
OPENDYSLEXIC_FONT_SIZES=(8 10 12 14)
|
||||
|
||||
# Custom font sizes - modify this array to change sizes for user-provided fonts
|
||||
CUSTOM_FONT_SIZES=(12 14 16 18)
|
||||
|
||||
for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
||||
for style in ${READER_FONT_STYLES[@]}; do
|
||||
font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
@ -53,3 +56,209 @@ for size in ${UI_FONT_SIZES[@]}; do
|
||||
done
|
||||
|
||||
python fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf > ../builtinFonts/notosans_8_regular.h
|
||||
|
||||
# ============================================================================
|
||||
# Custom Fonts Processing
|
||||
# ============================================================================
|
||||
# Process all custom fonts in the custom/ folder
|
||||
# Each subfolder should contain TTF files named: FontName-Regular.ttf, FontName-Bold.ttf, etc.
|
||||
|
||||
CUSTOM_DIR="../builtinFonts/custom"
|
||||
CUSTOM_HEADER="../builtinFonts/custom/customFonts.h"
|
||||
|
||||
# Collect custom font names
|
||||
declare -a CUSTOM_FONT_NAMES=()
|
||||
declare -a CUSTOM_FONT_LOWERCASE=()
|
||||
|
||||
if [ -d "$CUSTOM_DIR" ]; then
|
||||
for font_dir in "$CUSTOM_DIR"/*/; do
|
||||
if [ -d "$font_dir" ]; then
|
||||
# Get the font folder name (e.g., "FernMicro")
|
||||
font_folder_name=$(basename "$font_dir")
|
||||
font_name_lower=$(echo "$font_folder_name" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Check if Regular font exists (required)
|
||||
regular_font=$(find "$font_dir" -maxdepth 1 -iname "*-Regular.ttf" -o -iname "*-Regular.otf" 2>/dev/null | head -1)
|
||||
if [ -z "$regular_font" ]; then
|
||||
echo "Warning: Skipping $font_folder_name - no Regular font found"
|
||||
continue
|
||||
fi
|
||||
|
||||
CUSTOM_FONT_NAMES+=("$font_folder_name")
|
||||
CUSTOM_FONT_LOWERCASE+=("$font_name_lower")
|
||||
|
||||
echo "Processing custom font: $font_folder_name"
|
||||
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
for style in ${READER_FONT_STYLES[@]}; do
|
||||
style_lower=$(echo $style | tr '[:upper:]' '[:lower:]')
|
||||
output_name="${font_name_lower}_${size}_${style_lower}"
|
||||
output_path="../builtinFonts/custom/${output_name}.h"
|
||||
|
||||
# Find the font file for this style (try TTF then OTF)
|
||||
font_file=$(find "$font_dir" -maxdepth 1 -iname "*-${style}.ttf" -o -iname "*-${style}.otf" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$font_file" ]; then
|
||||
python fontconvert.py "$output_name" "$size" "$font_file" --2bit > "$output_path"
|
||||
echo "Generated $output_path"
|
||||
else
|
||||
# If style not found, use Regular as fallback
|
||||
echo "Note: $font_folder_name-${style} not found, using Regular"
|
||||
python fontconvert.py "$output_name" "$size" "$regular_font" --2bit > "$output_path"
|
||||
echo "Generated $output_path (fallback from Regular)"
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Generate customFonts.h registry (header with extern declarations)
|
||||
echo "Generating customFonts.h registry..."
|
||||
|
||||
CUSTOM_CPP="../../../src/customFonts.cpp"
|
||||
|
||||
cat > "$CUSTOM_HEADER" << 'HEADER_START'
|
||||
/**
|
||||
* Generated by convert-builtin-fonts.sh
|
||||
* Registry of available custom fonts
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <EpdFont.h>
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
HEADER_START
|
||||
|
||||
# Write the count
|
||||
echo "#define CUSTOM_FONT_COUNT ${#CUSTOM_FONT_NAMES[@]}" >> "$CUSTOM_HEADER"
|
||||
echo "" >> "$CUSTOM_HEADER"
|
||||
|
||||
# Write font names array
|
||||
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||
echo "static const char* CUSTOM_FONT_NAMES[] = {" >> "$CUSTOM_HEADER"
|
||||
for i in "${!CUSTOM_FONT_NAMES[@]}"; do
|
||||
if [ $i -lt $((${#CUSTOM_FONT_NAMES[@]} - 1)) ]; then
|
||||
echo " \"${CUSTOM_FONT_NAMES[$i]}\"," >> "$CUSTOM_HEADER"
|
||||
else
|
||||
echo " \"${CUSTOM_FONT_NAMES[$i]}\"" >> "$CUSTOM_HEADER"
|
||||
fi
|
||||
done
|
||||
echo "};" >> "$CUSTOM_HEADER"
|
||||
else
|
||||
echo "static const char* CUSTOM_FONT_NAMES[] = {};" >> "$CUSTOM_HEADER"
|
||||
fi
|
||||
|
||||
echo "" >> "$CUSTOM_HEADER"
|
||||
|
||||
# Include all generated headers in the header file
|
||||
echo "// Include all custom font headers" >> "$CUSTOM_HEADER"
|
||||
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
for style in ${READER_FONT_STYLES[@]}; do
|
||||
style_lower=$(echo $style | tr '[:upper:]' '[:lower:]')
|
||||
echo "#include <builtinFonts/custom/${font_name_lower}_${size}_${style_lower}.h>" >> "$CUSTOM_HEADER"
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo "" >> "$CUSTOM_HEADER"
|
||||
|
||||
# Generate extern declarations for EpdFont and EpdFontFamily in header
|
||||
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||
echo "// Extern EpdFont declarations for custom fonts" >> "$CUSTOM_HEADER"
|
||||
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
for style in ${READER_FONT_STYLES[@]}; do
|
||||
var_name="${font_name_lower}${size}${style}Font"
|
||||
echo "extern EpdFont ${var_name};" >> "$CUSTOM_HEADER"
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo "" >> "$CUSTOM_HEADER"
|
||||
echo "// Extern EpdFontFamily declarations for custom fonts" >> "$CUSTOM_HEADER"
|
||||
|
||||
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
family_name="${font_name_lower}${size}FontFamily"
|
||||
echo "extern EpdFontFamily ${family_name};" >> "$CUSTOM_HEADER"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> "$CUSTOM_HEADER"
|
||||
|
||||
# Function declaration in header
|
||||
echo "// Function to register all custom fonts with the renderer" >> "$CUSTOM_HEADER"
|
||||
echo "void registerCustomFonts(GfxRenderer& renderer);" >> "$CUSTOM_HEADER"
|
||||
echo "" >> "$CUSTOM_HEADER"
|
||||
|
||||
# Generate the .cpp file with actual definitions
|
||||
cat > "$CUSTOM_CPP" << 'CPP_START'
|
||||
/**
|
||||
* Generated by convert-builtin-fonts.sh
|
||||
* Custom font definitions
|
||||
*/
|
||||
#include <builtinFonts/custom/customFonts.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include "fontIds.h"
|
||||
|
||||
CPP_START
|
||||
|
||||
# Generate EpdFont definitions in .cpp
|
||||
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||
echo "// EpdFont definitions for custom fonts" >> "$CUSTOM_CPP"
|
||||
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
for style in ${READER_FONT_STYLES[@]}; do
|
||||
style_lower=$(echo $style | tr '[:upper:]' '[:lower:]')
|
||||
var_name="${font_name_lower}${size}${style}Font"
|
||||
data_name="${font_name_lower}_${size}_${style_lower}"
|
||||
echo "EpdFont ${var_name}(&${data_name});" >> "$CUSTOM_CPP"
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo "" >> "$CUSTOM_CPP"
|
||||
echo "// EpdFontFamily definitions for custom fonts" >> "$CUSTOM_CPP"
|
||||
|
||||
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
family_name="${font_name_lower}${size}FontFamily"
|
||||
regular="${font_name_lower}${size}RegularFont"
|
||||
bold="${font_name_lower}${size}BoldFont"
|
||||
italic="${font_name_lower}${size}ItalicFont"
|
||||
bolditalic="${font_name_lower}${size}BoldItalicFont"
|
||||
echo "EpdFontFamily ${family_name}(&${regular}, &${bold}, &${italic}, &${bolditalic});" >> "$CUSTOM_CPP"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> "$CUSTOM_CPP"
|
||||
|
||||
# Generate registerCustomFonts function in .cpp
|
||||
echo "void registerCustomFonts(GfxRenderer& renderer) {" >> "$CUSTOM_CPP"
|
||||
|
||||
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||
echo "#if CUSTOM_FONT_COUNT > 0" >> "$CUSTOM_CPP"
|
||||
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||
font_name_upper=$(echo "$font_name_lower" | tr '[:lower:]' '[:upper:]')
|
||||
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||
family_name="${font_name_lower}${size}FontFamily"
|
||||
echo " renderer.insertFont(${font_name_upper}_${size}_FONT_ID, ${family_name});" >> "$CUSTOM_CPP"
|
||||
done
|
||||
done
|
||||
echo "#else" >> "$CUSTOM_CPP"
|
||||
echo " (void)renderer; // Suppress unused parameter warning" >> "$CUSTOM_CPP"
|
||||
echo "#endif" >> "$CUSTOM_CPP"
|
||||
else
|
||||
echo " (void)renderer; // Suppress unused parameter warning" >> "$CUSTOM_CPP"
|
||||
fi
|
||||
|
||||
echo "}" >> "$CUSTOM_CPP"
|
||||
echo "" >> "$CUSTOM_CPP"
|
||||
|
||||
echo "Generated customFonts.h and customFonts.cpp with ${#CUSTOM_FONT_NAMES[@]} custom font(s)"
|
||||
|
||||
@ -86,6 +86,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
tocNavItem = opfParser.tocNavPath;
|
||||
}
|
||||
|
||||
// Copy CSS files to metadata
|
||||
bookMetadata.cssFiles = opfParser.cssFiles;
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
return true;
|
||||
}
|
||||
@ -204,6 +207,55 @@ bool Epub::parseTocNavFile() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseCssFiles() {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot parse CSS, cache not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||
cssParser.reset(new CssParser());
|
||||
|
||||
const auto& cssFiles = bookMetadataCache->coreMetadata.cssFiles;
|
||||
if (cssFiles.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& cssPath : cssFiles) {
|
||||
Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str());
|
||||
|
||||
// Extract CSS file to temp location
|
||||
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||
FsFile tempCssFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||
Serial.printf("[%lu] [EBP] Could not create temp CSS file\n", millis());
|
||||
continue;
|
||||
}
|
||||
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read CSS file: %s\n", millis(), cssPath.c_str());
|
||||
tempCssFile.close();
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
tempCssFile.close();
|
||||
|
||||
// Parse the CSS file
|
||||
if (!SdMan.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||
Serial.printf("[%lu] [EBP] Could not open temp CSS file for reading\n", millis());
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
cssParser->loadFromStream(tempCssFile);
|
||||
tempCssFile.close();
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files (~%zu bytes)\n", millis(),
|
||||
cssParser->ruleCount(), cssFiles.size(), cssParser->estimateMemoryUsage());
|
||||
return true;
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||
@ -213,6 +265,8 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
|
||||
// Try to load existing cache first
|
||||
if (bookMetadataCache->load()) {
|
||||
// Parse CSS files from loaded cache
|
||||
parseCssFiles();
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
@ -309,6 +363,9 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse CSS files after cache reload
|
||||
parseCssFiles();
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
@ -368,8 +425,7 @@ const std::string& Epub::getLanguage() const {
|
||||
}
|
||||
|
||||
std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
|
||||
return cachePath + "/" + coverFileName + ".bmp";
|
||||
return cropped ? (cachePath + "/cover_crop.bmp") : (cachePath + "/cover_fit.bmp");
|
||||
}
|
||||
|
||||
bool Epub::generateCoverBmp(bool cropped) const {
|
||||
@ -405,12 +461,37 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get JPEG dimensions to calculate target dimensions for FIT/CROP
|
||||
int jpegWidth, jpegHeight;
|
||||
if (!JpegToBmpConverter::getJpegDimensions(coverJpg, jpegWidth, jpegHeight)) {
|
||||
Serial.printf("[%lu] [EBP] Failed to get JPEG dimensions\n", millis());
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate target dimensions based on FIT/CROP mode
|
||||
// FIT: ancho fijo 480px, alto proporcional = 480 * (jpegHeight / jpegWidth)
|
||||
// CROP: alto fijo 800px, ancho proporcional = 800 * (jpegWidth / jpegHeight)
|
||||
int targetWidth, targetHeight;
|
||||
if (cropped) {
|
||||
// CROP mode: height = 800, width proportional
|
||||
targetHeight = 800;
|
||||
targetWidth = (800 * jpegWidth) / jpegHeight;
|
||||
} else {
|
||||
// FIT mode: width = 480, height proportional
|
||||
targetWidth = 480;
|
||||
targetHeight = (480 * jpegHeight) / jpegWidth;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Calculated %s dimensions: %dx%d (original JPEG: %dx%d)\n", millis(),
|
||||
cropped ? "CROP" : "FIT", targetWidth, targetHeight, jpegWidth, jpegHeight);
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight);
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
@ -492,6 +573,228 @@ bool Epub::generateThumbBmp() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Epub::getMicroThumbBmpPath() const { return cachePath + "/micro_thumb.bmp"; }
|
||||
|
||||
bool Epub::generateMicroThumbBmp() const {
|
||||
// Already generated, return true
|
||||
if (SdMan.exists(getMicroThumbBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate micro thumb BMP, cache not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image for micro thumbnail\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating micro thumb BMP from JPG cover image\n", millis());
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
// Check if temp JPEG already exists (from generateAllCovers), otherwise extract it
|
||||
bool needsCleanup = false;
|
||||
if (!SdMan.exists(coverJpgTempPath.c_str())) {
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
needsCleanup = true;
|
||||
}
|
||||
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile microThumbBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getMicroThumbBmpPath(), microThumbBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
// Use very small target size for Recent Books list (45x60 pixels)
|
||||
// Generate 1-bit BMP for fast rendering
|
||||
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
||||
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
||||
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
|
||||
coverJpg.close();
|
||||
microThumbBmp.close();
|
||||
|
||||
if (needsCleanup) {
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate micro thumb BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getMicroThumbBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated micro thumb BMP from JPG cover image, success: %s\n", millis(),
|
||||
success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping micro thumbnail\n", millis());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Epub::generateAllCovers(const std::function<void(int)>& progressCallback) const {
|
||||
// Check if all covers already exist - quick exit if nothing to do
|
||||
const bool hasThumb = SdMan.exists(getThumbBmpPath().c_str());
|
||||
const bool hasMicroThumb = SdMan.exists(getMicroThumbBmpPath().c_str());
|
||||
const bool hasCoverFit = SdMan.exists(getCoverBmpPath(false).c_str());
|
||||
const bool hasCoverCrop = SdMan.exists(getCoverBmpPath(true).c_str());
|
||||
|
||||
if (hasThumb && hasMicroThumb && hasCoverFit && hasCoverCrop) {
|
||||
Serial.printf("[%lu] [EBP] All covers already cached\n", millis());
|
||||
if (progressCallback) progressCallback(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate covers, cache not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only process JPG/JPEG covers
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) != ".jpg" &&
|
||||
coverImageHref.substr(coverImageHref.length() - 5) != ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping all cover generation\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Generating all covers (thumb:%d, micro:%d, fit:%d, crop:%d)\n", millis(), !hasThumb,
|
||||
!hasMicroThumb, !hasCoverFit, !hasCoverCrop);
|
||||
|
||||
// Extract JPEG once to temp file
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
{
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
Serial.printf("[%lu] [EBP] Failed to create temp cover file\n", millis());
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
}
|
||||
|
||||
// Get JPEG dimensions once for FIT/CROP calculations
|
||||
int jpegWidth = 0, jpegHeight = 0;
|
||||
{
|
||||
FsFile coverJpg;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
JpegToBmpConverter::getJpegDimensions(coverJpg, jpegWidth, jpegHeight);
|
||||
coverJpg.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Progress tracking: 4 covers = 25% each
|
||||
// Helper to create sub-progress callback that maps 0-100% to a portion of overall progress
|
||||
auto makeSubProgress = [&progressCallback](int startPercent, int endPercent) {
|
||||
if (!progressCallback) return std::function<void(int)>(nullptr);
|
||||
return std::function<void(int)>([&progressCallback, startPercent, endPercent](int subPercent) {
|
||||
const int overallProgress = startPercent + (subPercent * (endPercent - startPercent)) / 100;
|
||||
progressCallback(overallProgress);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate thumb (240x400, 1-bit) if missing - progress 0-25%
|
||||
if (!hasThumb) {
|
||||
FsFile coverJpg, thumbBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
||||
coverJpg, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT, makeSubProgress(0, 25));
|
||||
coverJpg.close();
|
||||
thumbBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getThumbBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated thumb: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(25);
|
||||
|
||||
// Generate micro thumb (45x60, 1-bit) if missing - progress 25-50%
|
||||
if (!hasMicroThumb) {
|
||||
FsFile coverJpg, microThumbBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getMicroThumbBmpPath(), microThumbBmp)) {
|
||||
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
||||
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
||||
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT, makeSubProgress(25, 50));
|
||||
coverJpg.close();
|
||||
microThumbBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getMicroThumbBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated micro thumb: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(50);
|
||||
|
||||
// Generate cover_fit (480xProportional, 2-bit) if missing - progress 50-75%
|
||||
if (!hasCoverFit && jpegWidth > 0 && jpegHeight > 0) {
|
||||
FsFile coverJpg, coverBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getCoverBmpPath(false), coverBmp)) {
|
||||
const int targetWidth = 480;
|
||||
const int targetHeight = (480 * jpegHeight) / jpegWidth;
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth,
|
||||
targetHeight, makeSubProgress(50, 75));
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getCoverBmpPath(false).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated cover_fit: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(75);
|
||||
|
||||
// Generate cover_crop (Proportionalx800, 2-bit) if missing - progress 75-100%
|
||||
if (!hasCoverCrop && jpegWidth > 0 && jpegHeight > 0) {
|
||||
FsFile coverJpg, coverBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getCoverBmpPath(true), coverBmp)) {
|
||||
const int targetHeight = 800;
|
||||
const int targetWidth = (800 * jpegWidth) / jpegHeight;
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth,
|
||||
targetHeight, makeSubProgress(75, 100));
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getCoverBmpPath(true).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated cover_crop: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(100);
|
||||
|
||||
// Clean up temp JPEG
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
Serial.printf("[%lu] [EBP] All cover generation complete\n", millis());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
if (itemHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
#include "Epub/css/CssParser.h"
|
||||
|
||||
class ZipFile;
|
||||
|
||||
@ -24,11 +26,14 @@ class Epub {
|
||||
std::string cachePath;
|
||||
// Spine and TOC cache
|
||||
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||
// CSS parser for styling
|
||||
std::unique_ptr<CssParser> cssParser;
|
||||
|
||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||
bool parseTocNcxFile() const;
|
||||
bool parseTocNavFile() const;
|
||||
bool parseCssFiles();
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
@ -49,6 +54,9 @@ class Epub {
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
bool generateThumbBmp() const;
|
||||
std::string getMicroThumbBmpPath() const;
|
||||
bool generateMicroThumbBmp() const;
|
||||
bool generateAllCovers(const std::function<void(int)>& progressCallback = nullptr) const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
@ -64,4 +72,5 @@ class Epub {
|
||||
|
||||
size_t getBookSize() const;
|
||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
const CssParser* getCssParser() const { return cssParser.get(); }
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
#include "FsHelpers.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 5;
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 6;
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
@ -115,9 +115,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
|
||||
constexpr uint32_t headerASize =
|
||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||
// Calculate CSS files size: count + each string (length + data)
|
||||
uint32_t cssFilesSize = sizeof(uint16_t); // count
|
||||
for (const auto& css : metadata.cssFiles) {
|
||||
cssFilesSize += sizeof(uint32_t) + css.size();
|
||||
}
|
||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
||||
sizeof(uint32_t) * 5;
|
||||
sizeof(uint32_t) * 5 + cssFilesSize;
|
||||
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||
const uint32_t lutOffset = headerASize + metadataSize;
|
||||
|
||||
@ -132,6 +137,11 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
serialization::writeString(bookFile, metadata.language);
|
||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||
// CSS files
|
||||
serialization::writePod(bookFile, static_cast<uint16_t>(metadata.cssFiles.size()));
|
||||
for (const auto& css : metadata.cssFiles) {
|
||||
serialization::writeString(bookFile, css);
|
||||
}
|
||||
|
||||
// Loop through spine entries, writing LUT positions
|
||||
spineFile.seek(0);
|
||||
@ -385,6 +395,16 @@ bool BookMetadataCache::load() {
|
||||
serialization::readString(bookFile, coreMetadata.language);
|
||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||
// CSS files
|
||||
uint16_t cssCount;
|
||||
serialization::readPod(bookFile, cssCount);
|
||||
coreMetadata.cssFiles.clear();
|
||||
coreMetadata.cssFiles.reserve(cssCount);
|
||||
for (uint16_t i = 0; i < cssCount; i++) {
|
||||
std::string cssPath;
|
||||
serialization::readString(bookFile, cssPath);
|
||||
coreMetadata.cssFiles.push_back(std::move(cssPath));
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
|
||||
@ -14,6 +14,7 @@ class BookMetadataCache {
|
||||
std::string language;
|
||||
std::string coverItemHref;
|
||||
std::string textReferenceHref;
|
||||
std::vector<std::string> cssFiles;
|
||||
};
|
||||
|
||||
struct SpineEntry {
|
||||
|
||||
@ -25,6 +25,29 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||
}
|
||||
|
||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
// Images don't use fontId or text rendering
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageImage::serialize(FsFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
|
||||
// serialize ImageBlock
|
||||
return imageBlock->serialize(file);
|
||||
}
|
||||
|
||||
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||
int16_t xPos;
|
||||
int16_t yPos;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
|
||||
auto ib = ImageBlock::deserialize(file);
|
||||
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||
}
|
||||
|
||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||
for (auto& element : elements) {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
@ -36,8 +59,9 @@ bool Page::serialize(FsFile& file) const {
|
||||
serialization::writePod(file, count);
|
||||
|
||||
for (const auto& el : elements) {
|
||||
// Only PageLine exists currently
|
||||
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
||||
// Use getTag() method to determine type
|
||||
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||
|
||||
if (!el->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
@ -59,6 +83,9 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
if (tag == TAG_PageLine) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else if (tag == TAG_PageImage) {
|
||||
auto pi = PageImage::deserialize(file);
|
||||
page->elements.push_back(std::move(pi));
|
||||
} else {
|
||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||
return nullptr;
|
||||
|
||||
@ -4,10 +4,12 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/ImageBlock.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageImage = 2, // New tag
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
@ -19,6 +21,7 @@ class PageElement {
|
||||
virtual ~PageElement() = default;
|
||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||
virtual bool serialize(FsFile& file) = 0;
|
||||
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||
};
|
||||
|
||||
// a line from a block element
|
||||
@ -30,13 +33,36 @@ class PageLine final : public PageElement {
|
||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||
|
||||
// Getter for word selection support
|
||||
const std::shared_ptr<TextBlock>& getTextBlock() const { return block; }
|
||||
};
|
||||
|
||||
// New PageImage class
|
||||
class PageImage final : public PageElement {
|
||||
std::shared_ptr<ImageBlock> imageBlock;
|
||||
|
||||
public:
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
// the list of block index and line numbers on this page
|
||||
std::vector<std::shared_ptr<PageElement>> elements;
|
||||
|
||||
// Byte offset in source HTML where this page's content begins
|
||||
// Used for restoring reading position after re-indexing due to font/setting changes
|
||||
// This is stored in the Section file's LUT, not in Page serialization
|
||||
uint32_t firstContentOffset = 0;
|
||||
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(FsFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||
|
||||
@ -49,11 +49,12 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
||||
|
||||
} // namespace
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) {
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline) {
|
||||
if (word.empty()) return;
|
||||
|
||||
words.push_back(std::move(word));
|
||||
wordStyles.push_back(fontStyle);
|
||||
wordUnderlines.push_back(underline);
|
||||
}
|
||||
|
||||
// Consumes data to minimize memory usage
|
||||
@ -67,7 +68,9 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
// Apply fixed transforms before any per-line layout work.
|
||||
applyParagraphIndent();
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
// Apply horizontal margin (for blockquotes, nested content, etc.)
|
||||
const int leftMargin = blockStyle.marginLeft;
|
||||
const int pageWidth = viewportWidth - leftMargin;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
@ -80,7 +83,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
|
||||
extractLine(i, pageWidth, spaceWidth, leftMargin, wordWidths, lineBreakIndices, processLine);
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,9 +95,21 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
|
||||
auto wordsIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
bool isFirst = true;
|
||||
|
||||
while (wordsIt != words.end()) {
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
||||
uint16_t width = measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt);
|
||||
|
||||
// Add CSS text-indent to first word width
|
||||
if (isFirst && blockStyle.textIndent > 0 && (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) &&
|
||||
!extraParagraphSpacing) {
|
||||
width += static_cast<uint16_t>(blockStyle.textIndent);
|
||||
isFirst = false;
|
||||
} else {
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
wordWidths.push_back(width);
|
||||
|
||||
std::advance(wordsIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
@ -200,7 +215,10 @@ void ParsedText::applyParagraphIndent() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
|
||||
if (blockStyle.textIndent > 0) {
|
||||
// CSS text-indent is handled via first word width adjustment
|
||||
// We'll add the indent value directly to the first word's width
|
||||
} else if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
|
||||
words.front().insert(0, "\xe2\x80\x83");
|
||||
}
|
||||
}
|
||||
@ -265,14 +283,9 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get iterators to target word and style.
|
||||
auto wordIt = words.begin();
|
||||
auto styleIt = wordStyles.begin();
|
||||
std::advance(wordIt, wordIndex);
|
||||
std::advance(styleIt, wordIndex);
|
||||
|
||||
const std::string& word = *wordIt;
|
||||
const auto style = *styleIt;
|
||||
// Direct index access for vectors (more efficient than iterator + advance)
|
||||
const std::string& word = words[wordIndex];
|
||||
const auto wordStyle = wordStyles[wordIndex];
|
||||
|
||||
// Collect candidate breakpoints (byte offsets and hyphen requirements).
|
||||
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
|
||||
@ -292,7 +305,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
}
|
||||
|
||||
const bool needsHyphen = info.requiresInsertedHyphen;
|
||||
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen);
|
||||
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), wordStyle, needsHyphen);
|
||||
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
|
||||
continue; // Skip if too wide or not an improvement
|
||||
}
|
||||
@ -309,25 +322,23 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
|
||||
// Split the word at the selected breakpoint and append a hyphen if required.
|
||||
std::string remainder = word.substr(chosenOffset);
|
||||
wordIt->resize(chosenOffset);
|
||||
words[wordIndex].resize(chosenOffset);
|
||||
if (chosenNeedsHyphen) {
|
||||
wordIt->push_back('-');
|
||||
words[wordIndex].push_back('-');
|
||||
}
|
||||
|
||||
// Insert the remainder word (with matching style) directly after the prefix.
|
||||
auto insertWordIt = std::next(wordIt);
|
||||
auto insertStyleIt = std::next(styleIt);
|
||||
words.insert(insertWordIt, remainder);
|
||||
wordStyles.insert(insertStyleIt, style);
|
||||
words.insert(words.begin() + wordIndex + 1, remainder);
|
||||
wordStyles.insert(wordStyles.begin() + wordIndex + 1, wordStyle);
|
||||
|
||||
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
||||
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
|
||||
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, wordStyle);
|
||||
wordWidths.insert(wordWidths.begin() + wordIndex + 1, remainderWidth);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, const int leftMargin,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
@ -350,33 +361,35 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
}
|
||||
|
||||
// Calculate initial x position
|
||||
uint16_t xpos = 0;
|
||||
// Calculate initial x position (offset by left margin for blockquotes, etc.)
|
||||
uint16_t xpos = static_cast<uint16_t>(leftMargin);
|
||||
if (style == TextBlock::RIGHT_ALIGN) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
xpos += spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
xpos += (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
std::list<uint16_t> lineXPos;
|
||||
std::vector<uint16_t> lineXPos;
|
||||
lineXPos.reserve(lineWordCount);
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
const uint16_t currentWordWidth = wordWidths[i];
|
||||
lineXPos.push_back(xpos);
|
||||
xpos += currentWordWidth + spacing;
|
||||
}
|
||||
|
||||
// Iterators always start at the beginning as we are moving content with splice below
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
// *** CRITICAL STEP: CONSUME DATA USING MOVE + ERASE ***
|
||||
// Move first lineWordCount elements from words into lineWords
|
||||
std::vector<std::string> lineWords(std::make_move_iterator(words.begin()),
|
||||
std::make_move_iterator(words.begin() + lineWordCount));
|
||||
words.erase(words.begin(), words.begin() + lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
std::vector<EpdFontFamily::Style> lineWordStyles(std::make_move_iterator(wordStyles.begin()),
|
||||
std::make_move_iterator(wordStyles.begin() + lineWordCount));
|
||||
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + lineWordCount);
|
||||
|
||||
std::vector<bool> lineWordUnderlines(wordUnderlines.begin(), wordUnderlines.begin() + lineWordCount);
|
||||
wordUnderlines.erase(wordUnderlines.begin(), wordUnderlines.begin() + lineWordCount);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
if (containsSoftHyphen(word)) {
|
||||
@ -384,5 +397,6 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
}
|
||||
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
||||
}
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style,
|
||||
blockStyle, std::move(lineWordUnderlines)));
|
||||
}
|
||||
|
||||
@ -3,19 +3,21 @@
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/BlockStyle.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
class ParsedText {
|
||||
std::list<std::string> words;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<std::string> words;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordUnderlines; // Track underline per word
|
||||
TextBlock::Style style;
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
|
||||
@ -26,20 +28,25 @@ class ParsedText {
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
||||
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, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, int leftMargin,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
public:
|
||||
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
|
||||
const bool hyphenationEnabled = false)
|
||||
: style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||
const bool hyphenationEnabled = false, const BlockStyle& blockStyle = BlockStyle())
|
||||
: style(style),
|
||||
blockStyle(blockStyle),
|
||||
extraParagraphSpacing(extraParagraphSpacing),
|
||||
hyphenationEnabled(hyphenationEnabled) {}
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle);
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false);
|
||||
void setStyle(const TextBlock::Style style) { this->style = style; }
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
TextBlock::Style getStyle() const { return style; }
|
||||
const BlockStyle& getBlockStyle() const { 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,
|
||||
|
||||
@ -8,10 +8,15 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 10;
|
||||
// Version 13: Added marginLeft and hasLeftBorder to BlockStyle serialization
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
||||
sizeof(uint32_t);
|
||||
|
||||
// LUT entry structure: { filePosition, contentOffset }
|
||||
// Each entry is 8 bytes (2 x uint32_t)
|
||||
constexpr size_t LUT_ENTRY_SIZE = sizeof(uint32_t) * 2;
|
||||
} // namespace
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
@ -181,33 +186,57 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled);
|
||||
std::vector<uint32_t> lut = {};
|
||||
|
||||
// LUT entries: { filePosition, contentOffset } pairs
|
||||
struct LutEntry {
|
||||
uint32_t filePos;
|
||||
uint32_t contentOffset;
|
||||
};
|
||||
std::vector<LutEntry> lut = {};
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||
progressFn);
|
||||
[this, &lut](std::unique_ptr<Page> page) {
|
||||
// Capture content offset before processing
|
||||
const uint32_t contentOffset = page->firstContentOffset;
|
||||
const uint32_t filePos = this->onPageComplete(std::move(page));
|
||||
lut.push_back({filePos, contentOffset});
|
||||
},
|
||||
progressFn, epub->getCssParser());
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
SdMan.remove(tmpHtmlPath.c_str());
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
||||
file.close();
|
||||
SdMan.remove(filePath.c_str());
|
||||
return false;
|
||||
Serial.printf("[%lu] [SCT] Failed to parse XML, creating placeholder page for chapter\n", millis());
|
||||
// Create a placeholder page for malformed chapters instead of failing entirely
|
||||
// This allows the book to continue loading with chapters that do parse successfully
|
||||
auto placeholderPage = std::unique_ptr<Page>(new Page());
|
||||
placeholderPage->firstContentOffset = 0;
|
||||
// Add placeholder to LUT
|
||||
const uint32_t filePos = this->onPageComplete(std::move(placeholderPage));
|
||||
lut.push_back({filePos, 0});
|
||||
|
||||
// If we still have no pages, the placeholder creation failed
|
||||
if (pageCount == 0) {
|
||||
Serial.printf("[%lu] [SCT] Failed to create placeholder page\n", millis());
|
||||
file.close();
|
||||
SdMan.remove(filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t lutOffset = file.position();
|
||||
bool hasFailedLutRecords = false;
|
||||
// Write LUT
|
||||
for (const uint32_t& pos : lut) {
|
||||
if (pos == 0) {
|
||||
// Write LUT with both file position and content offset
|
||||
for (const auto& entry : lut) {
|
||||
if (entry.filePos == 0) {
|
||||
hasFailedLutRecords = true;
|
||||
break;
|
||||
}
|
||||
serialization::writePod(file, pos);
|
||||
serialization::writePod(file, entry.filePos);
|
||||
serialization::writePod(file, entry.contentOffset);
|
||||
}
|
||||
|
||||
if (hasFailedLutRecords) {
|
||||
@ -233,12 +262,106 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(file, lutOffset);
|
||||
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
||||
|
||||
// LUT entries are now 8 bytes each: { filePos (4), contentOffset (4) }
|
||||
file.seek(lutOffset + LUT_ENTRY_SIZE * currentPage);
|
||||
uint32_t pagePos;
|
||||
serialization::readPod(file, pagePos);
|
||||
// Skip contentOffset for now - we don't need it when just loading the page
|
||||
|
||||
file.seek(pagePos);
|
||||
|
||||
auto page = Page::deserialize(file);
|
||||
file.close();
|
||||
return page;
|
||||
}
|
||||
|
||||
int Section::findPageForContentOffset(uint32_t targetOffset) const {
|
||||
if (pageCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FsFile f;
|
||||
if (!SdMan.openFileForRead("SCT", filePath, f)) {
|
||||
Serial.printf("[%lu] [SCT] findPageForContentOffset: Failed to open file\n", millis());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read LUT offset from header
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(f, lutOffset);
|
||||
|
||||
// Binary search through the LUT to find the page containing targetOffset
|
||||
// We want the largest contentOffset that is <= targetOffset
|
||||
int left = 0;
|
||||
int right = pageCount - 1;
|
||||
int result = 0;
|
||||
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
|
||||
// Read content offset for page 'mid'
|
||||
// LUT entry format: { filePos (4), contentOffset (4) }
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * mid + sizeof(uint32_t)); // Skip filePos
|
||||
uint32_t midOffset;
|
||||
serialization::readPod(f, midOffset);
|
||||
|
||||
if (midOffset <= targetOffset) {
|
||||
result = mid; // This page could be the answer
|
||||
left = mid + 1; // Look for a later page that might also qualify
|
||||
} else {
|
||||
right = mid - 1; // Look for an earlier page
|
||||
}
|
||||
}
|
||||
|
||||
// When multiple pages share the same content offset (e.g., a large text
|
||||
// block spanning multiple pages), scan backward to find the FIRST page
|
||||
// with that offset, not the last
|
||||
if (result > 0) {
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * result + sizeof(uint32_t));
|
||||
uint32_t resultOffset;
|
||||
serialization::readPod(f, resultOffset);
|
||||
|
||||
while (result > 0) {
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * (result - 1) + sizeof(uint32_t));
|
||||
uint32_t prevOffset;
|
||||
serialization::readPod(f, prevOffset);
|
||||
if (prevOffset == resultOffset) {
|
||||
result--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.close();
|
||||
Serial.printf("[%lu] [SCT] findPageForContentOffset: offset %u -> page %d\n", millis(), targetOffset, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t Section::getContentOffsetForPage(int pageIndex) const {
|
||||
if (pageCount == 0 || pageIndex < 0 || pageIndex >= pageCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FsFile f;
|
||||
if (!SdMan.openFileForRead("SCT", filePath, f)) {
|
||||
Serial.printf("[%lu] [SCT] getContentOffsetForPage: Failed to open file\n", millis());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read LUT offset from header
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(f, lutOffset);
|
||||
|
||||
// Read content offset for the specified page
|
||||
// LUT entry format: { filePos (4), contentOffset (4) }
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * pageIndex + sizeof(uint32_t)); // Skip filePos
|
||||
uint32_t contentOffset;
|
||||
serialization::readPod(f, contentOffset);
|
||||
|
||||
f.close();
|
||||
return contentOffset;
|
||||
}
|
||||
|
||||
@ -36,4 +36,9 @@ class Section {
|
||||
const std::function<void()>& progressSetupFn = nullptr,
|
||||
const std::function<void(int)>& progressFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
|
||||
// Methods for content offset-based position tracking
|
||||
// Used to restore reading position after re-indexing due to font/setting changes
|
||||
int findPageForContentOffset(uint32_t targetOffset) const;
|
||||
uint32_t getContentOffsetForPage(int pageIndex) const;
|
||||
};
|
||||
|
||||
19
lib/Epub/Epub/blocks/BlockStyle.h
Normal file
19
lib/Epub/Epub/blocks/BlockStyle.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* BlockStyle - Block-level CSS properties for paragraphs
|
||||
*
|
||||
* Used to track margin/padding spacing and text indentation for block elements.
|
||||
* Padding is treated similarly to margins for rendering purposes.
|
||||
*/
|
||||
struct BlockStyle {
|
||||
int8_t marginTop = 0; // 0-2 lines
|
||||
int8_t marginBottom = 0; // 0-2 lines
|
||||
int8_t paddingTop = 0; // 0-2 lines (treated same as margin)
|
||||
int8_t paddingBottom = 0; // 0-2 lines (treated same as margin)
|
||||
int16_t textIndent = 0; // pixels (first line indent)
|
||||
int16_t marginLeft = 0; // pixels (horizontal indent for entire block)
|
||||
bool hasLeftBorder = false; // draw vertical bar in left margin (for blockquotes)
|
||||
};
|
||||
173
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
173
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
|
||||
// Cache file format:
|
||||
// - uint16_t width
|
||||
// - uint16_t height
|
||||
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||
|
||||
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||
: imagePath(imagePath), width(width), height(height) {}
|
||||
|
||||
bool ImageBlock::imageExists() const {
|
||||
FsFile file;
|
||||
return SdMan.openFileForRead("IMG", imagePath, file);
|
||||
}
|
||||
|
||||
void ImageBlock::layout(GfxRenderer& renderer) {}
|
||||
|
||||
static 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";
|
||||
}
|
||||
|
||||
static bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
FsFile cacheFile;
|
||||
if (!SdMan.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
Serial.printf("[%lu] [IMG] Cache dimension mismatch: %dx%d vs %dx%d\n", millis(), cachedWidth, cachedHeight,
|
||||
expectedWidth, expectedHeight);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||
expectedWidth = cachedWidth;
|
||||
expectedHeight = cachedHeight;
|
||||
|
||||
Serial.printf("[%lu] [IMG] Loading from cache: %s (%dx%d)\n", millis(), cachePath.c_str(), cachedWidth, cachedHeight);
|
||||
|
||||
// Read and render row by row to minimize memory usage
|
||||
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
if (!rowBuffer) {
|
||||
Serial.printf("[%lu] [IMG] Failed to allocate row buffer\n", millis());
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||
Serial.printf("[%lu] [IMG] Cache read error at row %d\n", millis(), row);
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int destY = y + row;
|
||||
for (int col = 0; col < cachedWidth; col++) {
|
||||
int byteIdx = col / 4;
|
||||
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
renderer.drawPixel(x + col, destY, pixelValue < 2);
|
||||
}
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
Serial.printf("[%lu] [IMG] Cache render complete\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
Serial.printf("[%lu] [IMG] Rendering image at %d,%d: %s (%dx%d)\n", millis(), x, y, imagePath.c_str(), width, height);
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Bounds check render position using logical screen dimensions
|
||||
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||
Serial.printf("[%lu] [IMG] Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)\n", millis(), x, y, width,
|
||||
height, screenWidth, screenHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
return; // Successfully rendered from cache
|
||||
}
|
||||
|
||||
// No cache - need to decode the image
|
||||
// Check if image file exists
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("IMG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [IMG] Image file not found: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
file.close();
|
||||
|
||||
if (fileSize == 0) {
|
||||
Serial.printf("[%lu] [IMG] Image file is empty: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Decoding and caching: %s\n", millis(), 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.cachePath = cachePath; // Enable caching during decode
|
||||
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
Serial.printf("[%lu] [IMG] No decoder found for image: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Using %s decoder\n", millis(), decoder->getFormatName());
|
||||
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [IMG] Failed to decode image: %s\n", millis(), imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [IMG] Decode successful\n", millis());
|
||||
}
|
||||
|
||||
bool ImageBlock::serialize(FsFile& file) {
|
||||
serialization::writeString(file, imagePath);
|
||||
serialization::writePod(file, width);
|
||||
serialization::writePod(file, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
||||
std::string path;
|
||||
serialization::readString(file, path);
|
||||
int16_t w, h;
|
||||
serialization::readPod(file, w);
|
||||
serialization::readPod(file, h);
|
||||
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||
}
|
||||
32
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
32
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Block.h"
|
||||
|
||||
class ImageBlock final : public Block {
|
||||
public:
|
||||
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
||||
~ImageBlock() override = default;
|
||||
|
||||
const std::string& getImagePath() const { return imagePath; }
|
||||
int16_t getWidth() const { return width; }
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
bool imageExists() const;
|
||||
|
||||
void layout(GfxRenderer& renderer) override;
|
||||
BlockType getType() override { return IMAGE_BLOCK; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
void render(GfxRenderer& renderer, const int x, const int y);
|
||||
bool serialize(FsFile& file);
|
||||
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
||||
|
||||
private:
|
||||
std::string imagePath;
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
@ -11,16 +11,54 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw left border (vertical bar) for blockquotes
|
||||
if (blockStyle.hasLeftBorder && blockStyle.marginLeft > 0) {
|
||||
const int lineHeight = renderer.getLineHeight(fontId);
|
||||
const int barX = x + 4; // Small offset from left edge
|
||||
const int barTop = y;
|
||||
const int barBottom = y + lineHeight;
|
||||
// Draw a 2-pixel wide vertical bar
|
||||
renderer.drawLine(barX, barTop, barX, barBottom, true);
|
||||
renderer.drawLine(barX + 1, barTop, barX + 1, barBottom, true);
|
||||
}
|
||||
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
|
||||
auto wordUnderlineIt = wordUnderlines.begin();
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
||||
const int wordX = *wordXposIt + x;
|
||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, *wordStylesIt);
|
||||
|
||||
// Draw underline if word is underlined
|
||||
if (wordUnderlineIt != wordUnderlines.end() && *wordUnderlineIt) {
|
||||
const std::string& w = *wordIt;
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), *wordStylesIt);
|
||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||
|
||||
int startX = wordX;
|
||||
int underlineWidth = fullWordWidth;
|
||||
|
||||
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
|
||||
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
||||
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||
const char* visiblePtr = w.c_str() + 3;
|
||||
const int prefixWidth = renderer.getIndentWidth(fontId, std::string("\xe2\x80\x83").c_str());
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, *wordStylesIt);
|
||||
startX = wordX + prefixWidth;
|
||||
underlineWidth = visibleWidth;
|
||||
}
|
||||
|
||||
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
||||
}
|
||||
|
||||
std::advance(wordIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
std::advance(wordXposIt, 1);
|
||||
if (wordUnderlineIt != wordUnderlines.end()) {
|
||||
std::advance(wordUnderlineIt, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,29 +75,59 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||
|
||||
// Block style
|
||||
// Underline flags (packed as bytes, 8 words per byte)
|
||||
uint8_t underlineByte = 0;
|
||||
int bitIndex = 0;
|
||||
auto underlineIt = wordUnderlines.begin();
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
if (underlineIt != wordUnderlines.end() && *underlineIt) {
|
||||
underlineByte |= 1 << bitIndex;
|
||||
}
|
||||
bitIndex++;
|
||||
if (bitIndex == 8 || i == words.size() - 1) {
|
||||
serialization::writePod(file, underlineByte);
|
||||
underlineByte = 0;
|
||||
bitIndex = 0;
|
||||
}
|
||||
if (underlineIt != wordUnderlines.end()) {
|
||||
++underlineIt;
|
||||
}
|
||||
}
|
||||
|
||||
// Block style (alignment)
|
||||
serialization::writePod(file, style);
|
||||
|
||||
// Block style (margins/padding/indent)
|
||||
serialization::writePod(file, blockStyle.marginTop);
|
||||
serialization::writePod(file, blockStyle.marginBottom);
|
||||
serialization::writePod(file, blockStyle.paddingTop);
|
||||
serialization::writePod(file, blockStyle.paddingBottom);
|
||||
serialization::writePod(file, blockStyle.textIndent);
|
||||
serialization::writePod(file, blockStyle.marginLeft);
|
||||
serialization::writePod(file, blockStyle.hasLeftBorder);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
uint16_t wc;
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordUnderlines;
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
serialization::readPod(file, wc);
|
||||
|
||||
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
||||
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
||||
if (wc > 10000) {
|
||||
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Word data
|
||||
// Word data - reserve capacity then resize
|
||||
words.resize(wc);
|
||||
wordXpos.resize(wc);
|
||||
wordStyles.resize(wc);
|
||||
@ -67,8 +135,31 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
|
||||
// Block style
|
||||
// Underline flags (packed as bytes, 8 words per byte)
|
||||
wordUnderlines.resize(wc, false);
|
||||
size_t underlineIdx = 0;
|
||||
const int bytesNeeded = (wc + 7) / 8;
|
||||
for (int byteIdx = 0; byteIdx < bytesNeeded; byteIdx++) {
|
||||
uint8_t underlineByte;
|
||||
serialization::readPod(file, underlineByte);
|
||||
for (int bit = 0; bit < 8 && underlineIdx < wc; bit++) {
|
||||
wordUnderlines[underlineIdx] = (underlineByte & (1 << bit)) != 0;
|
||||
++underlineIdx;
|
||||
}
|
||||
}
|
||||
|
||||
// Block style (alignment)
|
||||
serialization::readPod(file, style);
|
||||
|
||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
||||
// Block style (margins/padding/indent)
|
||||
serialization::readPod(file, blockStyle.marginTop);
|
||||
serialization::readPod(file, blockStyle.marginBottom);
|
||||
serialization::readPod(file, blockStyle.paddingTop);
|
||||
serialization::readPod(file, blockStyle.paddingBottom);
|
||||
serialization::readPod(file, blockStyle.textIndent);
|
||||
serialization::readPod(file, blockStyle.marginLeft);
|
||||
serialization::readPod(file, blockStyle.hasLeftBorder);
|
||||
|
||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
||||
blockStyle, std::move(wordUnderlines)));
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
#include <EpdFontFamily.h>
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
|
||||
// Represents a line of text on a page
|
||||
class TextBlock final : public Block {
|
||||
@ -19,19 +20,41 @@ class TextBlock final : public Block {
|
||||
};
|
||||
|
||||
private:
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordUnderlines; // Track underline per word
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||
std::list<EpdFontFamily::Style> word_styles, const Style style)
|
||||
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
|
||||
std::vector<EpdFontFamily::Style> word_styles, const Style style,
|
||||
const BlockStyle& blockStyle = BlockStyle(),
|
||||
std::vector<bool> word_underlines = std::vector<bool>())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
wordUnderlines(std::move(word_underlines)),
|
||||
style(style),
|
||||
blockStyle(blockStyle) {
|
||||
// Ensure underlines list matches words list size
|
||||
while (this->wordUnderlines.size() < this->words.size()) {
|
||||
this->wordUnderlines.push_back(false);
|
||||
}
|
||||
}
|
||||
~TextBlock() override = default;
|
||||
void setStyle(const Style style) { this->style = style; }
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
Style getStyle() const { return style; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
|
||||
// Getters for word selection support
|
||||
const std::vector<std::string>& getWords() const { return words; }
|
||||
const std::vector<uint16_t>& getWordXPositions() const { return wordXpos; }
|
||||
const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
|
||||
size_t getWordCount() const { return words.size(); }
|
||||
void layout(GfxRenderer& renderer) override {};
|
||||
// given a renderer works out where to break the words into lines
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
|
||||
31
lib/Epub/Epub/converters/FramebufferWriter.cpp
Normal file
31
lib/Epub/Epub/converters/FramebufferWriter.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
#include "FramebufferWriter.h"
|
||||
|
||||
void FramebufferWriter::setPixel(int x, int y, bool isBlack) {
|
||||
if (x < 0 || x >= DISPLAY_WIDTH || y < 0 || y >= DISPLAY_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t byteIndex = y * DISPLAY_WIDTH_BYTES + (x / 8);
|
||||
const uint8_t bitPosition = 7 - (x % 8);
|
||||
|
||||
if (isBlack) {
|
||||
frameBuffer[byteIndex] &= ~(1 << bitPosition);
|
||||
} else {
|
||||
frameBuffer[byteIndex] |= (1 << bitPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void FramebufferWriter::setPixel2Bit(int x, int y, uint8_t value) {
|
||||
if (x < 0 || x >= DISPLAY_WIDTH || y < 0 || y >= DISPLAY_HEIGHT || value > 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t byteIndex = y * DISPLAY_WIDTH_BYTES + (x / 8);
|
||||
const uint8_t bitPosition = 7 - (x % 8);
|
||||
|
||||
if (value < 2) {
|
||||
frameBuffer[byteIndex] &= ~(1 << bitPosition);
|
||||
} else {
|
||||
frameBuffer[byteIndex] |= (1 << bitPosition);
|
||||
}
|
||||
}
|
||||
19
lib/Epub/Epub/converters/FramebufferWriter.h
Normal file
19
lib/Epub/Epub/converters/FramebufferWriter.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
class FramebufferWriter {
|
||||
private:
|
||||
uint8_t* frameBuffer;
|
||||
static constexpr int DISPLAY_WIDTH = 800;
|
||||
static constexpr int DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8; // 100
|
||||
static constexpr int DISPLAY_HEIGHT = 480;
|
||||
|
||||
public:
|
||||
explicit FramebufferWriter(uint8_t* framebuffer) : frameBuffer(framebuffer) {}
|
||||
|
||||
// Simple pixel setting for 1-bit rendering
|
||||
void setPixel(int x, int y, bool isBlack);
|
||||
|
||||
// 2-bit grayscale pixel setting (for dual-pass rendering)
|
||||
void setPixel2Bit(int x, int y, uint8_t value); // value: 0-3
|
||||
};
|
||||
68
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
68
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
#include "ImageDecoderFactory.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||
bool ImageDecoderFactory::initialized = false;
|
||||
|
||||
void ImageDecoderFactory::initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
jpegDecoder = std::unique_ptr<JpegToFramebufferConverter>(new JpegToFramebufferConverter());
|
||||
pngDecoder = std::unique_ptr<PngToFramebufferConverter>(new PngToFramebufferConverter());
|
||||
|
||||
initialized = true;
|
||||
Serial.printf("[%lu] [DEC] Image decoder factory initialized\n", millis());
|
||||
}
|
||||
|
||||
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||
if (!initialized) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
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 (jpegDecoder && jpegDecoder->supportsFormat(ext)) {
|
||||
return jpegDecoder.get();
|
||||
} else if (pngDecoder && pngDecoder->supportsFormat(ext)) {
|
||||
return pngDecoder.get();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [DEC] No decoder found for image: %s\n", millis(), imagePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) {
|
||||
if (jpegDecoder && jpegDecoder->supportsFormat(imagePath)) {
|
||||
return true;
|
||||
}
|
||||
if (pngDecoder && pngDecoder->supportsFormat(imagePath)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> ImageDecoderFactory::getSupportedFormats() {
|
||||
std::vector<std::string> formats;
|
||||
formats.push_back(".jpg");
|
||||
formats.push_back(".jpeg");
|
||||
formats.push_back(".png");
|
||||
return formats;
|
||||
}
|
||||
24
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
24
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class JpegToFramebufferConverter;
|
||||
class PngToFramebufferConverter;
|
||||
|
||||
class ImageDecoderFactory {
|
||||
public:
|
||||
static void initialize();
|
||||
// Returns non-owning pointer - factory owns the decoder lifetime
|
||||
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
|
||||
static bool isFormatSupported(const std::string& imagePath);
|
||||
static std::vector<std::string> getSupportedFormats();
|
||||
|
||||
private:
|
||||
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
|
||||
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
|
||||
static bool initialized;
|
||||
};
|
||||
18
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
18
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
@ -0,0 +1,18 @@
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||
if (width > MAX_SOURCE_WIDTH || height > MAX_SOURCE_HEIGHT) {
|
||||
Serial.printf("[%lu] [IMG] Image too large (%dx%d %s), max supported: %dx%d\n", millis(), width, height,
|
||||
format.c_str(), MAX_SOURCE_WIDTH, MAX_SOURCE_HEIGHT);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||
Serial.printf("[%lu] [IMG] Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.\n",
|
||||
millis(), feature.c_str(), imagePath.c_str());
|
||||
}
|
||||
43
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
43
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
struct ImageDimensions {
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
|
||||
struct RenderConfig {
|
||||
int x, y;
|
||||
int maxWidth, maxHeight;
|
||||
bool useGrayscale = true;
|
||||
bool useDithering = true;
|
||||
bool performanceMode = false;
|
||||
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 bool supportsFormat(const std::string& extension) const = 0;
|
||||
virtual const char* getFormatName() const = 0;
|
||||
|
||||
protected:
|
||||
// Size validation helpers
|
||||
// These limits are generous since picojpeg decodes MCU-by-MCU (no full image buffer needed)
|
||||
// Memory usage depends on OUTPUT size, not source size
|
||||
static constexpr int MAX_SOURCE_WIDTH = 3072;
|
||||
static constexpr int MAX_SOURCE_HEIGHT = 3072;
|
||||
|
||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||
};
|
||||
406
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
406
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
@ -0,0 +1,406 @@
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
#include <picojpeg.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
struct JpegContext {
|
||||
FsFile& file;
|
||||
uint8_t buffer[512];
|
||||
size_t bufferPos;
|
||||
size_t bufferFilled;
|
||||
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
|
||||
};
|
||||
|
||||
// Cache buffer for storing 2-bit pixels during decode
|
||||
struct PixelCache {
|
||||
uint8_t* buffer;
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
int originX; // config.x - to convert screen coords to cache coords
|
||||
int originY; // config.y
|
||||
|
||||
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
|
||||
|
||||
bool allocate(int w, int h, int ox, int oy) {
|
||||
width = w;
|
||||
height = h;
|
||||
originX = ox;
|
||||
originY = oy;
|
||||
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
size_t bufferSize = bytesPerRow * h;
|
||||
buffer = (uint8_t*)malloc(bufferSize);
|
||||
if (buffer) {
|
||||
memset(buffer, 0, bufferSize);
|
||||
Serial.printf("[%lu] [JPG] Allocated cache buffer: %d bytes for %dx%d\n", millis(), bufferSize, w, h);
|
||||
}
|
||||
return buffer != nullptr;
|
||||
}
|
||||
|
||||
void setPixel(int screenX, int screenY, uint8_t value) {
|
||||
if (!buffer) return;
|
||||
int localX = screenX - originX;
|
||||
int localY = screenY - originY;
|
||||
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
|
||||
|
||||
int byteIdx = localY * bytesPerRow + localX / 4;
|
||||
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
|
||||
bool writeToFile(const std::string& cachePath) {
|
||||
if (!buffer) return false;
|
||||
|
||||
FsFile cacheFile;
|
||||
if (!SdMan.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open cache file for writing: %s\n", millis(), cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t w = width;
|
||||
uint16_t h = height;
|
||||
cacheFile.write(&w, 2);
|
||||
cacheFile.write(&h, 2);
|
||||
cacheFile.write(buffer, bytesPerRow * height);
|
||||
cacheFile.close();
|
||||
|
||||
Serial.printf("[%lu] [JPG] Cache written: %s (%dx%d, %d bytes)\n", millis(), cachePath.c_str(), width, height,
|
||||
4 + bytesPerRow * height);
|
||||
return true;
|
||||
}
|
||||
|
||||
~PixelCache() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static int16_t ditherErrors[512][3];
|
||||
|
||||
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("JPG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open file for dimensions: %s\n", millis(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JpegContext context(file);
|
||||
pjpeg_image_info_t imageInfo;
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
file.close();
|
||||
|
||||
if (status != 0) {
|
||||
// Provide more informative error messages for common JPEG issues
|
||||
// Error codes from picojpeg.h:
|
||||
// 37 = PJPG_NOT_SINGLE_SCAN (multi-scan/progressive JPEG)
|
||||
// 49 = PJPG_UNSUPPORTED_MODE (progressive JPEG)
|
||||
if (status == 37 || status == 49) {
|
||||
Serial.printf("[%lu] [JPG] Progressive/multi-scan JPEG not supported (error %d): %s\n", millis(), status,
|
||||
imagePath.c_str());
|
||||
} else {
|
||||
Serial.printf("[%lu] [JPG] Failed to init JPEG (error %d): %s\n", millis(), status, imagePath.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = imageInfo.m_width;
|
||||
out.height = imageInfo.m_height;
|
||||
Serial.printf("[%lu] [JPG] Image dimensions: %dx%d\n", millis(), out.width, out.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t JpegToFramebufferConverter::applyAtkinsonDithering(uint8_t gray, int x, int y, int width) {
|
||||
int16_t error = gray - (gray < 128 ? 0 : 255);
|
||||
uint8_t newGray = gray - error;
|
||||
|
||||
int8_t fraction = error >> 3;
|
||||
|
||||
if (x + 1 < width && y + 1 < 512) ditherErrors[y + 1][(x + 1) % 3] += fraction;
|
||||
if (x + 2 < width && y + 1 < 512) ditherErrors[y + 1][(x + 2) % 3] += fraction;
|
||||
if (x + 1 < width) ditherErrors[y][(x + 1) % 3] += fraction;
|
||||
if (x + 2 < width) ditherErrors[y][(x + 2) % 3] += fraction;
|
||||
if (x - 1 >= 0 && x + 1 < width && y + 1 < 512) ditherErrors[y + 1][(x - 1 + 1) % 3] += fraction;
|
||||
if (x - 1 >= 0 && y + 1 < 512) ditherErrors[y + 1][(x - 1) % 3] += fraction;
|
||||
if (x + 1 < width && y + 2 < 512) ditherErrors[y + 2][(x + 1) % 3] += fraction;
|
||||
|
||||
int16_t adjustedGray = newGray + ditherErrors[y][x % 3];
|
||||
if (adjustedGray < 0) adjustedGray = 0;
|
||||
if (adjustedGray > 255) adjustedGray = 255;
|
||||
|
||||
uint8_t outputGray;
|
||||
if (adjustedGray < 64) {
|
||||
outputGray = 0;
|
||||
} else if (adjustedGray < 128) {
|
||||
outputGray = 1;
|
||||
} else if (adjustedGray < 192) {
|
||||
outputGray = 2;
|
||||
} else {
|
||||
outputGray = 3;
|
||||
}
|
||||
|
||||
ditherErrors[y][x % 3] = adjustedGray - (outputGray * 85);
|
||||
|
||||
return outputGray;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
Serial.printf("[%lu] [JPG] Decoding JPEG: %s\n", millis(), imagePath.c_str());
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("JPG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to open file: %s\n", millis(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(ditherErrors, 0, sizeof(ditherErrors));
|
||||
|
||||
JpegContext context(file);
|
||||
pjpeg_image_info_t imageInfo;
|
||||
|
||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||
if (status != 0) {
|
||||
// Error 37 = PJPG_NOT_SINGLE_SCAN, 49 = PJPG_UNSUPPORTED_MODE (progressive JPEG)
|
||||
if (status == 37 || status == 49) {
|
||||
Serial.printf("[%lu] [JPG] Progressive/multi-scan JPEG not supported (error %d)\n", millis(), status);
|
||||
} else {
|
||||
Serial.printf("[%lu] [JPG] picojpeg init failed (error %d)\n", millis(), status);
|
||||
}
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX =
|
||||
(config.maxWidth > 0 && imageInfo.m_width > config.maxWidth) ? (float)config.maxWidth / imageInfo.m_width : 1.0f;
|
||||
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
|
||||
? (float)config.maxHeight / imageInfo.m_height
|
||||
: 1.0f;
|
||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (scale > 1.0f) scale = 1.0f;
|
||||
|
||||
int destWidth = (int)(imageInfo.m_width * scale);
|
||||
int destHeight = (int)(imageInfo.m_height * scale);
|
||||
|
||||
Serial.printf("[%lu] [JPG] JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d\n", millis(),
|
||||
imageInfo.m_width, imageInfo.m_height, destWidth, destHeight, scale, imageInfo.m_scanType,
|
||||
imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
||||
|
||||
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
|
||||
Serial.printf("[%lu] [JPG] Null buffer pointers in imageInfo\n", millis());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Allocate pixel cache if cachePath is provided
|
||||
PixelCache cache;
|
||||
bool caching = !config.cachePath.empty();
|
||||
if (caching) {
|
||||
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to allocate cache buffer, continuing without caching\n", millis());
|
||||
caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
int mcuX = 0;
|
||||
int mcuY = 0;
|
||||
|
||||
while (mcuY < imageInfo.m_MCUSPerCol) {
|
||||
status = pjpeg_decode_mcu();
|
||||
if (status == PJPG_NO_MORE_BLOCKS) {
|
||||
break;
|
||||
}
|
||||
if (status != 0) {
|
||||
Serial.printf("[%lu] [JPG] MCU decode failed: %d\n", millis(), status);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Source position in image coordinates
|
||||
int srcStartX = mcuX * imageInfo.m_MCUWidth;
|
||||
int srcStartY = mcuY * imageInfo.m_MCUHeight;
|
||||
|
||||
switch (imageInfo.m_scanType) {
|
||||
case PJPG_GRAYSCALE:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||
uint8_t dithered =
|
||||
config.useDithering ? applyAtkinsonDithering(gray, destX, destY, screenWidth) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
renderer.drawPixel(destX, destY, dithered < 2);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH1V1:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered =
|
||||
config.useDithering ? applyAtkinsonDithering(gray, destX, destY, screenWidth) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
renderer.drawPixel(destX, destY, dithered < 2);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH2V1:
|
||||
for (int row = 0; row < 8; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 16; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockIndex = (col < 8) ? 0 : 1;
|
||||
int pixelIndex = row * 8 + (col % 8);
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered =
|
||||
config.useDithering ? applyAtkinsonDithering(gray, destX, destY, screenWidth) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
renderer.drawPixel(destX, destY, dithered < 2);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH1V2:
|
||||
for (int row = 0; row < 16; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockIndex = (row < 8) ? 0 : 1;
|
||||
int pixelIndex = (row % 8) * 8 + col;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered =
|
||||
config.useDithering ? applyAtkinsonDithering(gray, destX, destY, screenWidth) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
renderer.drawPixel(destX, destY, dithered < 2);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PJPG_YH2V2:
|
||||
for (int row = 0; row < 16; row++) {
|
||||
int srcY = srcStartY + row;
|
||||
int destY = config.y + (int)(srcY * scale);
|
||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||
for (int col = 0; col < 16; col++) {
|
||||
int srcX = srcStartX + col;
|
||||
int destX = config.x + (int)(srcX * scale);
|
||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||
int blockX = (col < 8) ? 0 : 1;
|
||||
int blockY = (row < 8) ? 0 : 1;
|
||||
int blockIndex = blockY * 2 + blockX;
|
||||
int pixelIndex = (row % 8) * 8 + (col % 8);
|
||||
int blockOffset = blockIndex * 64;
|
||||
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
|
||||
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
|
||||
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
|
||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||
uint8_t dithered =
|
||||
config.useDithering ? applyAtkinsonDithering(gray, destX, destY, screenWidth) : gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
renderer.drawPixel(destX, destY, dithered < 2);
|
||||
if (caching) cache.setPixel(destX, destY, dithered);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
mcuX++;
|
||||
if (mcuX >= imageInfo.m_MCUSPerRow) {
|
||||
mcuX = 0;
|
||||
mcuY++;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [JPG] Decoding complete\n", millis());
|
||||
file.close();
|
||||
|
||||
// Write cache file if caching was enabled
|
||||
if (caching) {
|
||||
cache.writeToFile(config.cachePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
||||
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
|
||||
|
||||
if (context->bufferPos >= context->bufferFilled) {
|
||||
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
|
||||
if (readCount <= 0) {
|
||||
*pBytes_actually_read = 0;
|
||||
return 0;
|
||||
}
|
||||
context->bufferFilled = readCount;
|
||||
context->bufferPos = 0;
|
||||
}
|
||||
|
||||
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
|
||||
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
|
||||
|
||||
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
|
||||
context->bufferPos += bytesToCopy;
|
||||
*pBytes_actually_read = bytesToCopy;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) const {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".jpg" || ext == ".jpeg");
|
||||
}
|
||||
25
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
25
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
@ -0,0 +1,25 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
bool supportsFormat(const std::string& extension) const override;
|
||||
const char* getFormatName() const override { return "JPEG"; }
|
||||
|
||||
private:
|
||||
uint8_t applyAtkinsonDithering(uint8_t gray, int x, int y, int width);
|
||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||
};
|
||||
354
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
354
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
@ -0,0 +1,354 @@
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <PNGdec.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <SdFat.h>
|
||||
|
||||
static FsFile* gPngFile = nullptr;
|
||||
|
||||
static void* pngOpenForDims(const char* filename, int32_t* size) { return gPngFile; }
|
||||
|
||||
static void pngCloseForDims(void* handle) {}
|
||||
|
||||
static int32_t pngReadForDims(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
if (!gPngFile) return 0;
|
||||
return gPngFile->read(pBuf, len);
|
||||
}
|
||||
|
||||
static int32_t pngSeekForDims(PNGFILE* pFile, int32_t pos) {
|
||||
if (!gPngFile) return -1;
|
||||
return gPngFile->seek(pos);
|
||||
}
|
||||
|
||||
// Single static PNG object shared between getDimensions and decode
|
||||
// (these operations never happen simultaneously)
|
||||
static PNG png;
|
||||
|
||||
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("PNG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open file for dimensions: %s\n", millis(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
gPngFile = &file;
|
||||
|
||||
int rc = png.open(imagePath.c_str(), pngOpenForDims, pngCloseForDims, pngReadForDims, pngSeekForDims, nullptr);
|
||||
|
||||
if (rc != 0) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open PNG for dimensions: %d\n", millis(), rc);
|
||||
file.close();
|
||||
gPngFile = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = png.getWidth();
|
||||
out.height = png.getHeight();
|
||||
|
||||
png.close();
|
||||
file.close();
|
||||
gPngFile = nullptr;
|
||||
return true;
|
||||
}
|
||||
static GfxRenderer* gRenderer = nullptr;
|
||||
static const RenderConfig* gConfig = nullptr;
|
||||
static int gScreenWidth = 0;
|
||||
static int gScreenHeight = 0;
|
||||
static int16_t ditherErrors[2048][3];
|
||||
static FsFile* pngFile = nullptr;
|
||||
|
||||
// Scaling state for PNG
|
||||
static float gScale = 1.0f;
|
||||
static int gSrcWidth = 0;
|
||||
static int gSrcHeight = 0;
|
||||
static int gDstWidth = 0;
|
||||
static int gDstHeight = 0;
|
||||
static int gLastDstY = -1; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
// Pixel cache for PNG (uses scaled dimensions)
|
||||
static uint8_t* gCacheBuffer = nullptr;
|
||||
static int gCacheWidth = 0;
|
||||
static int gCacheHeight = 0;
|
||||
static int gCacheBytesPerRow = 0;
|
||||
static int gCacheOriginX = 0;
|
||||
static int gCacheOriginY = 0;
|
||||
|
||||
static void cacheSetPixel(int screenX, int screenY, uint8_t value) {
|
||||
if (!gCacheBuffer) return;
|
||||
int localX = screenX - gCacheOriginX;
|
||||
int localY = screenY - gCacheOriginY;
|
||||
if (localX < 0 || localX >= gCacheWidth || localY < 0 || localY >= gCacheHeight) return;
|
||||
|
||||
int byteIdx = localY * gCacheBytesPerRow + localX / 4;
|
||||
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
gCacheBuffer[byteIdx] = (gCacheBuffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
|
||||
static uint8_t applyAtkinsonDithering(uint8_t gray, int x, int y, int width) {
|
||||
int16_t error = gray - (gray < 128 ? 0 : 255);
|
||||
uint8_t newGray = gray - error;
|
||||
|
||||
int8_t fraction = error >> 3;
|
||||
|
||||
if (x + 1 < width) ditherErrors[y + 1][(x + 1) % 3] += fraction;
|
||||
if (x + 2 < width) ditherErrors[y + 1][(x + 2) % 3] += fraction;
|
||||
if (x + 1 < width) ditherErrors[y][(x + 1) % 3] += fraction;
|
||||
if (x + 2 < width) ditherErrors[y][(x + 2) % 3] += fraction;
|
||||
if (x - 1 >= 0 && x + 1 < width) ditherErrors[y + 1][(x - 1 + 1) % 3] += fraction;
|
||||
if (x - 1 >= 0) ditherErrors[y + 1][(x - 1) % 3] += fraction;
|
||||
if (x + 1 < width) ditherErrors[y + 2][(x + 1) % 3] += fraction;
|
||||
|
||||
int16_t adjustedGray = newGray + ditherErrors[y][x % 3];
|
||||
if (adjustedGray < 0) adjustedGray = 0;
|
||||
if (adjustedGray > 255) adjustedGray = 255;
|
||||
|
||||
uint8_t outputGray;
|
||||
if (adjustedGray < 64) {
|
||||
outputGray = 0;
|
||||
} else if (adjustedGray < 128) {
|
||||
outputGray = 1;
|
||||
} else if (adjustedGray < 192) {
|
||||
outputGray = 2;
|
||||
} else {
|
||||
outputGray = 3;
|
||||
}
|
||||
|
||||
ditherErrors[y][x % 3] = adjustedGray - (outputGray * 85);
|
||||
|
||||
return outputGray;
|
||||
}
|
||||
|
||||
void* pngOpen(const char* filename, int32_t* size) {
|
||||
pngFile = new FsFile();
|
||||
if (!SdMan.openFileForRead("PNG", std::string(filename), *pngFile)) {
|
||||
delete pngFile;
|
||||
pngFile = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
*size = pngFile->size();
|
||||
return pngFile;
|
||||
}
|
||||
|
||||
void pngClose(void* handle) {
|
||||
if (pngFile) {
|
||||
pngFile->close();
|
||||
delete pngFile;
|
||||
pngFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t pngRead(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
if (!pngFile) return 0;
|
||||
return pngFile->read(pBuf, len);
|
||||
}
|
||||
|
||||
int32_t pngSeek(PNGFILE* pFile, int32_t pos) {
|
||||
if (!pngFile) return -1;
|
||||
return pngFile->seek(pos);
|
||||
}
|
||||
|
||||
// Helper to get grayscale from PNG pixel data
|
||||
static uint8_t getGrayFromPixel(uint8_t* pPixels, int x, int pixelType, uint8_t* palette) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
return pPixels[x];
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR: {
|
||||
uint8_t* p = &pPixels[x * 3];
|
||||
return (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
|
||||
case PNG_PIXEL_INDEXED: {
|
||||
uint8_t paletteIndex = pPixels[x];
|
||||
if (palette) {
|
||||
uint8_t* p = &palette[paletteIndex * 3];
|
||||
return (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
return paletteIndex;
|
||||
}
|
||||
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
return pPixels[x * 2];
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA: {
|
||||
uint8_t* p = &pPixels[x * 4];
|
||||
return (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
|
||||
default:
|
||||
return 128;
|
||||
}
|
||||
}
|
||||
|
||||
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
if (!gConfig || !gRenderer) return 0;
|
||||
|
||||
int srcY = pDraw->y;
|
||||
uint8_t* pPixels = pDraw->pPixels;
|
||||
int pixelType = pDraw->iPixelType;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * gScale);
|
||||
|
||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||
if (dstY == gLastDstY) return 1;
|
||||
gLastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= gDstHeight) return 1;
|
||||
|
||||
int outY = gConfig->y + dstY;
|
||||
if (outY >= gScreenHeight) return 1;
|
||||
|
||||
// Render scaled row using nearest-neighbor sampling
|
||||
for (int dstX = 0; dstX < gDstWidth; dstX++) {
|
||||
int outX = gConfig->x + dstX;
|
||||
if (outX >= gScreenWidth) continue;
|
||||
|
||||
// Map destination X back to source X
|
||||
int srcX = (int)(dstX / gScale);
|
||||
if (srcX >= gSrcWidth) srcX = gSrcWidth - 1;
|
||||
|
||||
uint8_t gray = getGrayFromPixel(pPixels, srcX, pixelType, pDraw->pPalette);
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (gConfig->useDithering) {
|
||||
ditheredGray = applyAtkinsonDithering(gray, outX, outY, gScreenWidth);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
gRenderer->drawPixel(outX, outY, ditheredGray < 2);
|
||||
cacheSetPixel(outX, outY, ditheredGray);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
Serial.printf("[%lu] [PNG] Decoding PNG: %s\n", millis(), imagePath.c_str());
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("PNG", imagePath, file)) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open file: %s\n", millis(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(ditherErrors, 0, sizeof(ditherErrors));
|
||||
gRenderer = &renderer;
|
||||
gConfig = &config;
|
||||
gScreenWidth = renderer.getScreenWidth();
|
||||
gScreenHeight = renderer.getScreenHeight();
|
||||
|
||||
int rc = png.open(imagePath.c_str(), pngOpen, pngClose, pngRead, pngSeek, pngDrawCallback);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("[%lu] [PNG] Failed to open PNG: %d\n", millis(), rc);
|
||||
file.close();
|
||||
gRenderer = nullptr;
|
||||
gConfig = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(png.getWidth(), png.getHeight(), "PNG")) {
|
||||
png.close();
|
||||
file.close();
|
||||
gRenderer = nullptr;
|
||||
gConfig = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate scale factor to fit within maxWidth x maxHeight
|
||||
gSrcWidth = png.getWidth();
|
||||
gSrcHeight = png.getHeight();
|
||||
float scaleX = (float)config.maxWidth / gSrcWidth;
|
||||
float scaleY = (float)config.maxHeight / gSrcHeight;
|
||||
gScale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (gScale > 1.0f) gScale = 1.0f; // Don't upscale
|
||||
|
||||
gDstWidth = (int)(gSrcWidth * gScale);
|
||||
gDstHeight = (int)(gSrcHeight * gScale);
|
||||
gLastDstY = -1; // Reset row tracking
|
||||
|
||||
Serial.printf("[%lu] [PNG] PNG %dx%d -> %dx%d (scale %.2f), bpp: %d\n", millis(), gSrcWidth, gSrcHeight, gDstWidth,
|
||||
gDstHeight, gScale, png.getBpp());
|
||||
|
||||
if (png.getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png.getBpp()) + "bpp)", imagePath);
|
||||
}
|
||||
|
||||
if (png.hasAlpha()) {
|
||||
warnUnsupportedFeature("alpha channel", imagePath);
|
||||
}
|
||||
|
||||
// Allocate cache buffer using SCALED dimensions
|
||||
bool caching = !config.cachePath.empty();
|
||||
if (caching) {
|
||||
gCacheWidth = gDstWidth;
|
||||
gCacheHeight = gDstHeight;
|
||||
gCacheBytesPerRow = (gCacheWidth + 3) / 4;
|
||||
gCacheOriginX = config.x;
|
||||
gCacheOriginY = config.y;
|
||||
size_t bufferSize = gCacheBytesPerRow * gCacheHeight;
|
||||
gCacheBuffer = (uint8_t*)malloc(bufferSize);
|
||||
if (gCacheBuffer) {
|
||||
memset(gCacheBuffer, 0, bufferSize);
|
||||
Serial.printf("[%lu] [PNG] Allocated cache buffer: %d bytes for %dx%d\n", millis(), bufferSize, gCacheWidth,
|
||||
gCacheHeight);
|
||||
} else {
|
||||
Serial.printf("[%lu] [PNG] Failed to allocate cache buffer, continuing without caching\n", millis());
|
||||
caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
rc = png.decode(nullptr, 0);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("[%lu] [PNG] Decode failed: %d\n", millis(), rc);
|
||||
png.close();
|
||||
file.close();
|
||||
gRenderer = nullptr;
|
||||
gConfig = nullptr;
|
||||
if (gCacheBuffer) {
|
||||
free(gCacheBuffer);
|
||||
gCacheBuffer = nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
png.close();
|
||||
file.close();
|
||||
Serial.printf("[%lu] [PNG] PNG decoding complete\n", millis());
|
||||
|
||||
// Write cache file if caching was enabled and buffer was allocated
|
||||
if (caching && gCacheBuffer) {
|
||||
FsFile cacheFile;
|
||||
if (SdMan.openFileForWrite("IMG", config.cachePath, cacheFile)) {
|
||||
uint16_t w = gCacheWidth;
|
||||
uint16_t h = gCacheHeight;
|
||||
cacheFile.write(&w, 2);
|
||||
cacheFile.write(&h, 2);
|
||||
cacheFile.write(gCacheBuffer, gCacheBytesPerRow * gCacheHeight);
|
||||
cacheFile.close();
|
||||
Serial.printf("[%lu] [PNG] Cache written: %s (%dx%d, %d bytes)\n", millis(), config.cachePath.c_str(),
|
||||
gCacheWidth, gCacheHeight, 4 + gCacheBytesPerRow * gCacheHeight);
|
||||
} else {
|
||||
Serial.printf("[%lu] [PNG] Failed to open cache file for writing: %s\n", millis(), config.cachePath.c_str());
|
||||
}
|
||||
free(gCacheBuffer);
|
||||
gCacheBuffer = nullptr;
|
||||
}
|
||||
|
||||
gRenderer = nullptr;
|
||||
gConfig = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) const {
|
||||
std::string ext = extension;
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
return (ext == ".png");
|
||||
}
|
||||
19
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
19
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <PNGdec.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
bool supportsFormat(const std::string& extension) const override;
|
||||
const char* getFormatName() const override { return "PNG"; }
|
||||
};
|
||||
524
lib/Epub/Epub/css/CssParser.cpp
Normal file
524
lib/Epub/Epub/css/CssParser.cpp
Normal file
@ -0,0 +1,524 @@
|
||||
#include "CssParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace {
|
||||
|
||||
// Buffer size for reading CSS files
|
||||
constexpr size_t READ_BUFFER_SIZE = 512;
|
||||
|
||||
// Maximum CSS file size we'll process (prevent memory issues)
|
||||
constexpr size_t MAX_CSS_SIZE = 64 * 1024;
|
||||
|
||||
// Check if character is CSS whitespace
|
||||
bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||
|
||||
// Read entire file into string (with size limit)
|
||||
std::string readFileContent(FsFile& file) {
|
||||
std::string content;
|
||||
content.reserve(std::min(static_cast<size_t>(file.size()), MAX_CSS_SIZE));
|
||||
|
||||
char buffer[READ_BUFFER_SIZE];
|
||||
while (file.available() && content.size() < MAX_CSS_SIZE) {
|
||||
const int bytesRead = file.read(buffer, sizeof(buffer));
|
||||
if (bytesRead <= 0) break;
|
||||
content.append(buffer, bytesRead);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Remove CSS comments (/* ... */) from content
|
||||
std::string stripComments(const std::string& css) {
|
||||
std::string result;
|
||||
result.reserve(css.size());
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < css.size()) {
|
||||
// Look for start of comment
|
||||
if (pos + 1 < css.size() && css[pos] == '/' && css[pos + 1] == '*') {
|
||||
// Find end of comment
|
||||
const size_t endPos = css.find("*/", pos + 2);
|
||||
if (endPos == std::string::npos) {
|
||||
// Unterminated comment - skip rest of file
|
||||
break;
|
||||
}
|
||||
pos = endPos + 2;
|
||||
} else {
|
||||
result.push_back(css[pos]);
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Skip @-rules (like @media, @import, @font-face)
|
||||
// Returns position after the @-rule
|
||||
size_t skipAtRule(const std::string& css, const size_t start) {
|
||||
// Find the end - either semicolon (simple @-rule) or matching brace
|
||||
size_t pos = start + 1; // Skip the '@'
|
||||
|
||||
// Skip identifier
|
||||
while (pos < css.size() && (std::isalnum(css[pos]) || css[pos] == '-')) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Look for { or ;
|
||||
int braceDepth = 0;
|
||||
while (pos < css.size()) {
|
||||
const char c = css[pos];
|
||||
if (c == '{') {
|
||||
++braceDepth;
|
||||
} else if (c == '}') {
|
||||
--braceDepth;
|
||||
if (braceDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
} else if (c == ';' && braceDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
return css.size();
|
||||
}
|
||||
|
||||
// Extract next rule from CSS content
|
||||
// Returns true if a rule was found, with selector and body filled
|
||||
bool extractNextRule(const std::string& css, size_t& pos, std::string& selector, std::string& body) {
|
||||
selector.clear();
|
||||
body.clear();
|
||||
|
||||
// Skip whitespace and @-rules until we find a regular rule
|
||||
while (pos < css.size()) {
|
||||
// Skip whitespace
|
||||
while (pos < css.size() && isCssWhitespace(css[pos])) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
if (pos >= css.size()) return false;
|
||||
|
||||
// Handle @-rules iteratively (avoids recursion/stack overflow)
|
||||
if (css[pos] == '@') {
|
||||
pos = skipAtRule(css, pos);
|
||||
continue; // Try again after skipping the @-rule
|
||||
}
|
||||
|
||||
break; // Found start of a regular rule
|
||||
}
|
||||
|
||||
if (pos >= css.size()) return false;
|
||||
|
||||
// Find opening brace
|
||||
const size_t bracePos = css.find('{', pos);
|
||||
if (bracePos == std::string::npos) return false;
|
||||
|
||||
// Extract selector (everything before the brace)
|
||||
selector = css.substr(pos, bracePos - pos);
|
||||
|
||||
// Find matching closing brace
|
||||
int depth = 1;
|
||||
const size_t bodyStart = bracePos + 1;
|
||||
size_t bodyEnd = bodyStart;
|
||||
|
||||
while (bodyEnd < css.size() && depth > 0) {
|
||||
if (css[bodyEnd] == '{')
|
||||
++depth;
|
||||
else if (css[bodyEnd] == '}')
|
||||
--depth;
|
||||
++bodyEnd;
|
||||
}
|
||||
|
||||
// Extract body (between braces)
|
||||
if (bodyEnd > bodyStart) {
|
||||
body = css.substr(bodyStart, bodyEnd - bodyStart - 1);
|
||||
}
|
||||
|
||||
pos = bodyEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// String utilities implementation
|
||||
|
||||
std::string CssParser::normalized(const std::string& s) {
|
||||
std::string result;
|
||||
result.reserve(s.size());
|
||||
|
||||
bool inSpace = true; // Start true to skip leading space
|
||||
for (const char c : s) {
|
||||
if (isCssWhitespace(c)) {
|
||||
if (!inSpace) {
|
||||
result.push_back(' ');
|
||||
inSpace = true;
|
||||
}
|
||||
} else {
|
||||
result.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing space
|
||||
if (!result.empty() && result.back() == ' ') {
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> CssParser::splitOnChar(const std::string& s, const char delimiter) {
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
|
||||
for (size_t i = 0; i <= s.size(); ++i) {
|
||||
if (i == s.size() || s[i] == delimiter) {
|
||||
std::string part = s.substr(start, i - start);
|
||||
std::string trimmed = normalized(part);
|
||||
if (!trimmed.empty()) {
|
||||
parts.push_back(trimmed);
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
std::vector<std::string> CssParser::splitWhitespace(const std::string& s) {
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
bool inWord = false;
|
||||
|
||||
for (size_t i = 0; i <= s.size(); ++i) {
|
||||
const bool isSpace = i == s.size() || isCssWhitespace(s[i]);
|
||||
if (isSpace && inWord) {
|
||||
parts.push_back(s.substr(start, i - start));
|
||||
inWord = false;
|
||||
} else if (!isSpace && !inWord) {
|
||||
start = i;
|
||||
inWord = true;
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
// Property value interpreters
|
||||
|
||||
TextAlign CssParser::interpretAlignment(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
|
||||
if (v == "left" || v == "start") return TextAlign::Left;
|
||||
if (v == "right" || v == "end") return TextAlign::Right;
|
||||
if (v == "center") return TextAlign::Center;
|
||||
if (v == "justify") return TextAlign::Justify;
|
||||
|
||||
return TextAlign::None;
|
||||
}
|
||||
|
||||
CssFontStyle CssParser::interpretFontStyle(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
|
||||
if (v == "italic" || v == "oblique") return CssFontStyle::Italic;
|
||||
return CssFontStyle::Normal;
|
||||
}
|
||||
|
||||
CssFontWeight CssParser::interpretFontWeight(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
|
||||
// Named values
|
||||
if (v == "bold" || v == "bolder") return CssFontWeight::Bold;
|
||||
if (v == "normal" || v == "lighter") return CssFontWeight::Normal;
|
||||
|
||||
// Numeric values: 100-900
|
||||
// CSS spec: 400 = normal, 700 = bold
|
||||
// We use: 0-400 = normal, 700+ = bold, 500-600 = normal (conservative)
|
||||
char* endPtr = nullptr;
|
||||
const long numericWeight = std::strtol(v.c_str(), &endPtr, 10);
|
||||
|
||||
// If we parsed a number and consumed the whole string
|
||||
if (endPtr != v.c_str() && *endPtr == '\0') {
|
||||
return numericWeight >= 700 ? CssFontWeight::Bold : CssFontWeight::Normal;
|
||||
}
|
||||
|
||||
return CssFontWeight::Normal;
|
||||
}
|
||||
|
||||
CssTextDecoration CssParser::interpretDecoration(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
|
||||
// text-decoration can have multiple space-separated values
|
||||
if (v.find("underline") != std::string::npos) {
|
||||
return CssTextDecoration::Underline;
|
||||
}
|
||||
return CssTextDecoration::None;
|
||||
}
|
||||
|
||||
float CssParser::interpretLength(const std::string& val, const float emSize) {
|
||||
const std::string v = normalized(val);
|
||||
if (v.empty()) return 0.0f;
|
||||
|
||||
// Determine unit and multiplier
|
||||
float multiplier = 1.0f;
|
||||
size_t unitStart = v.size();
|
||||
|
||||
// Find where the number ends
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
const char c = v[i];
|
||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||
unitStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string numPart = v.substr(0, unitStart);
|
||||
const std::string unitPart = v.substr(unitStart);
|
||||
|
||||
// Handle units
|
||||
if (unitPart == "em" || unitPart == "rem") {
|
||||
multiplier = emSize;
|
||||
} else if (unitPart == "pt") {
|
||||
multiplier = 1.33f; // Approximate pt to px conversion
|
||||
}
|
||||
// px is default (multiplier = 1.0)
|
||||
|
||||
char* endPtr = nullptr;
|
||||
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
||||
|
||||
if (endPtr == numPart.c_str()) return 0.0f; // No number parsed
|
||||
|
||||
return numericValue * multiplier;
|
||||
}
|
||||
|
||||
int8_t CssParser::interpretSpacing(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
if (v.empty()) return 0;
|
||||
|
||||
// For spacing, we convert to "lines" (discrete units for e-ink)
|
||||
// 1em ≈ 1 line, percentages based on ~30 lines per page
|
||||
|
||||
float multiplier = 0.0f;
|
||||
size_t unitStart = v.size();
|
||||
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
const char c = v[i];
|
||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||
unitStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string numPart = v.substr(0, unitStart);
|
||||
const std::string unitPart = v.substr(unitStart);
|
||||
|
||||
if (unitPart == "em" || unitPart == "rem") {
|
||||
multiplier = 1.0f; // 1em = 1 line
|
||||
} else if (unitPart == "%") {
|
||||
multiplier = 0.3f; // ~30 lines per page, so 10% = 3 lines
|
||||
} else {
|
||||
return 0; // Unsupported unit for spacing
|
||||
}
|
||||
|
||||
char* endPtr = nullptr;
|
||||
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
||||
|
||||
if (endPtr == numPart.c_str()) return 0;
|
||||
|
||||
int lines = static_cast<int>(numericValue * multiplier);
|
||||
|
||||
// Clamp to reasonable range (0-2 lines)
|
||||
if (lines < 0) lines = 0;
|
||||
if (lines > 2) lines = 2;
|
||||
|
||||
return static_cast<int8_t>(lines);
|
||||
}
|
||||
|
||||
// Declaration parsing
|
||||
|
||||
CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
CssStyle style;
|
||||
|
||||
// Split declarations by semicolon
|
||||
const auto declarations = splitOnChar(declBlock, ';');
|
||||
|
||||
for (const auto& decl : declarations) {
|
||||
// Find colon separator
|
||||
const size_t colonPos = decl.find(':');
|
||||
if (colonPos == std::string::npos || colonPos == 0) continue;
|
||||
|
||||
std::string propName = normalized(decl.substr(0, colonPos));
|
||||
std::string propValue = normalized(decl.substr(colonPos + 1));
|
||||
|
||||
if (propName.empty() || propValue.empty()) continue;
|
||||
|
||||
// Match property and set value
|
||||
if (propName == "text-align") {
|
||||
const TextAlign align = interpretAlignment(propValue);
|
||||
if (align != TextAlign::None) {
|
||||
style.alignment = align;
|
||||
style.defined.alignment = 1;
|
||||
}
|
||||
} else if (propName == "font-style") {
|
||||
style.fontStyle = interpretFontStyle(propValue);
|
||||
style.defined.fontStyle = 1;
|
||||
} else if (propName == "font-weight") {
|
||||
style.fontWeight = interpretFontWeight(propValue);
|
||||
style.defined.fontWeight = 1;
|
||||
} else if (propName == "text-decoration" || propName == "text-decoration-line") {
|
||||
style.decoration = interpretDecoration(propValue);
|
||||
style.defined.decoration = 1;
|
||||
} else if (propName == "text-indent") {
|
||||
style.indentPixels = interpretLength(propValue);
|
||||
style.defined.indent = 1;
|
||||
} else if (propName == "margin-top") {
|
||||
const int8_t spacing = interpretSpacing(propValue);
|
||||
if (spacing > 0) {
|
||||
style.marginTop = spacing;
|
||||
style.defined.marginTop = 1;
|
||||
}
|
||||
} else if (propName == "margin-bottom") {
|
||||
const int8_t spacing = interpretSpacing(propValue);
|
||||
if (spacing > 0) {
|
||||
style.marginBottom = spacing;
|
||||
style.defined.marginBottom = 1;
|
||||
}
|
||||
} else if (propName == "padding-top") {
|
||||
const int8_t spacing = interpretSpacing(propValue);
|
||||
if (spacing > 0) {
|
||||
style.paddingTop = spacing;
|
||||
style.defined.paddingTop = 1;
|
||||
}
|
||||
} else if (propName == "padding-bottom") {
|
||||
const int8_t spacing = interpretSpacing(propValue);
|
||||
if (spacing > 0) {
|
||||
style.paddingBottom = spacing;
|
||||
style.defined.paddingBottom = 1;
|
||||
}
|
||||
} else if (propName == "margin-left" || propName == "padding-left") {
|
||||
// Horizontal indentation for blockquotes and nested content
|
||||
const float pixels = interpretLength(propValue);
|
||||
if (pixels > 0) {
|
||||
style.marginLeft += pixels; // Accumulate margin-left and padding-left
|
||||
style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (propName == "margin") {
|
||||
// Shorthand: margin: top right bottom left OR margin: vertical horizontal
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (values.size() >= 2) {
|
||||
// At least 2 values: first is vertical (top/bottom), second is horizontal (left/right)
|
||||
const float horizontal = interpretLength(values[1]);
|
||||
if (horizontal > 0) {
|
||||
style.marginLeft = horizontal;
|
||||
style.defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
if (values.size() == 4) {
|
||||
// 4 values: top right bottom left - use the 4th value for left
|
||||
const float left = interpretLength(values[3]);
|
||||
if (left > 0) {
|
||||
style.marginLeft = left;
|
||||
style.defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
// Rule processing
|
||||
|
||||
void CssParser::processRuleBlock(const std::string& selectorGroup, const std::string& declarations) {
|
||||
const CssStyle style = parseDeclarations(declarations);
|
||||
|
||||
// Only store if any properties were set
|
||||
if (!style.defined.anySet()) return;
|
||||
|
||||
// Handle comma-separated selectors
|
||||
const auto selectors = splitOnChar(selectorGroup, ',');
|
||||
|
||||
for (const auto& sel : selectors) {
|
||||
// Normalize the selector
|
||||
std::string key = normalized(sel);
|
||||
if (key.empty()) continue;
|
||||
|
||||
// Store or merge with existing
|
||||
auto it = rulesBySelector_.find(key);
|
||||
if (it != rulesBySelector_.end()) {
|
||||
it->second.applyOver(style);
|
||||
} else {
|
||||
rulesBySelector_[key] = style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main parsing entry point
|
||||
|
||||
bool CssParser::loadFromStream(FsFile& source) {
|
||||
if (!source) {
|
||||
Serial.printf("[%lu] [CSS] Cannot read from invalid file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const std::string content = readFileContent(source);
|
||||
if (content.empty()) {
|
||||
return true; // Empty file is valid
|
||||
}
|
||||
|
||||
// Remove comments
|
||||
const std::string cleaned = stripComments(content);
|
||||
|
||||
// Parse rules
|
||||
size_t pos = 0;
|
||||
std::string selector, body;
|
||||
|
||||
while (extractNextRule(cleaned, pos, selector, body)) {
|
||||
processRuleBlock(selector, body);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Parsed %zu rules\n", millis(), rulesBySelector_.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Style resolution
|
||||
|
||||
CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const {
|
||||
CssStyle result;
|
||||
const std::string tag = normalized(tagName);
|
||||
|
||||
// 1. Apply element-level style (lowest priority)
|
||||
const auto tagIt = rulesBySelector_.find(tag);
|
||||
if (tagIt != rulesBySelector_.end()) {
|
||||
result.applyOver(tagIt->second);
|
||||
}
|
||||
|
||||
// 2. Apply class styles (medium priority)
|
||||
if (!classAttr.empty()) {
|
||||
const auto classes = splitWhitespace(classAttr);
|
||||
|
||||
for (const auto& cls : classes) {
|
||||
std::string classKey = "." + normalized(cls);
|
||||
|
||||
auto classIt = rulesBySelector_.find(classKey);
|
||||
if (classIt != rulesBySelector_.end()) {
|
||||
result.applyOver(classIt->second);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Apply element.class styles (higher priority)
|
||||
for (const auto& cls : classes) {
|
||||
std::string combinedKey = tag + "." + normalized(cls);
|
||||
|
||||
auto combinedIt = rulesBySelector_.find(combinedKey);
|
||||
if (combinedIt != rulesBySelector_.end()) {
|
||||
result.applyOver(combinedIt->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Inline style parsing (static - doesn't need rule database)
|
||||
|
||||
CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return parseDeclarations(styleValue); }
|
||||
118
lib/Epub/Epub/css/CssParser.h
Normal file
118
lib/Epub/Epub/css/CssParser.h
Normal file
@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "CssStyle.h"
|
||||
|
||||
/**
|
||||
* Lightweight CSS parser for EPUB stylesheets
|
||||
*
|
||||
* Parses CSS files and extracts styling information relevant for e-ink display.
|
||||
* Uses a two-phase approach: first tokenizes the CSS content, then builds
|
||||
* a rule database that can be queried during HTML parsing.
|
||||
*
|
||||
* Supported selectors:
|
||||
* - Element selectors: p, div, h1, etc.
|
||||
* - Class selectors: .classname
|
||||
* - Combined: element.classname
|
||||
* - Grouped: selector1, selector2 { }
|
||||
*
|
||||
* Not supported (silently ignored):
|
||||
* - Descendant/child selectors
|
||||
* - Pseudo-classes and pseudo-elements
|
||||
* - Media queries (content is skipped)
|
||||
* - @import, @font-face, etc.
|
||||
*/
|
||||
class CssParser {
|
||||
public:
|
||||
CssParser() = default;
|
||||
~CssParser() = default;
|
||||
|
||||
// Non-copyable
|
||||
CssParser(const CssParser&) = delete;
|
||||
CssParser& operator=(const CssParser&) = delete;
|
||||
|
||||
/**
|
||||
* Load and parse CSS from a file stream.
|
||||
* Can be called multiple times to accumulate rules from multiple stylesheets.
|
||||
* @param source Open file handle to read from
|
||||
* @return true if parsing completed (even if no rules found)
|
||||
*/
|
||||
bool loadFromStream(FsFile& source);
|
||||
|
||||
/**
|
||||
* Look up the style for an HTML element, considering tag name and class attributes.
|
||||
* Applies CSS cascade: element style < class style < element.class style
|
||||
*
|
||||
* @param tagName The HTML element name (e.g., "p", "div")
|
||||
* @param classAttr The class attribute value (may contain multiple space-separated classes)
|
||||
* @return Combined style with all applicable rules merged
|
||||
*/
|
||||
[[nodiscard]] CssStyle resolveStyle(const std::string& tagName, const std::string& classAttr) const;
|
||||
|
||||
/**
|
||||
* Parse an inline style attribute string.
|
||||
* @param styleValue The value of a style="" attribute
|
||||
* @return Parsed style properties
|
||||
*/
|
||||
[[nodiscard]] static CssStyle parseInlineStyle(const std::string& styleValue);
|
||||
|
||||
/**
|
||||
* Check if any rules have been loaded
|
||||
*/
|
||||
[[nodiscard]] bool empty() const { return rulesBySelector_.empty(); }
|
||||
|
||||
/**
|
||||
* Get count of loaded rule sets
|
||||
*/
|
||||
[[nodiscard]] size_t ruleCount() const { return rulesBySelector_.size(); }
|
||||
|
||||
/**
|
||||
* Estimate memory usage of loaded rules (for debugging)
|
||||
* Returns approximate bytes used by selector strings and style data
|
||||
*/
|
||||
[[nodiscard]] size_t estimateMemoryUsage() const {
|
||||
size_t bytes = 0;
|
||||
// unordered_map overhead: roughly 8 bytes per bucket + per-entry overhead
|
||||
bytes += rulesBySelector_.bucket_count() * sizeof(void*);
|
||||
for (const auto& entry : rulesBySelector_) {
|
||||
// String storage: capacity + SSO overhead (~24 bytes) + actual chars
|
||||
bytes += sizeof(std::string) + entry.first.capacity();
|
||||
// CssStyle is ~16 bytes
|
||||
bytes += sizeof(CssStyle);
|
||||
// Per-entry node overhead in unordered_map (~24-32 bytes)
|
||||
bytes += 32;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all loaded rules
|
||||
*/
|
||||
void clear() { rulesBySelector_.clear(); }
|
||||
|
||||
private:
|
||||
// Storage: maps normalized selector -> style properties
|
||||
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
||||
|
||||
// Internal parsing helpers
|
||||
void processRuleBlock(const std::string& selectorGroup, const std::string& declarations);
|
||||
static CssStyle parseDeclarations(const std::string& declBlock);
|
||||
|
||||
// Individual property value parsers
|
||||
static TextAlign interpretAlignment(const std::string& val);
|
||||
static CssFontStyle interpretFontStyle(const std::string& val);
|
||||
static CssFontWeight interpretFontWeight(const std::string& val);
|
||||
static CssTextDecoration interpretDecoration(const std::string& val);
|
||||
static float interpretLength(const std::string& val, float emSize = 16.0f);
|
||||
static int8_t interpretSpacing(const std::string& val);
|
||||
|
||||
// String utilities
|
||||
static std::string normalized(const std::string& s);
|
||||
static std::vector<std::string> splitOnChar(const std::string& s, char delimiter);
|
||||
static std::vector<std::string> splitWhitespace(const std::string& s);
|
||||
};
|
||||
142
lib/Epub/Epub/css/CssStyle.h
Normal file
142
lib/Epub/Epub/css/CssStyle.h
Normal file
@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Text alignment options matching CSS text-align property
|
||||
enum class TextAlign : uint8_t { None = 0, Left = 1, Right = 2, Center = 3, Justify = 4 };
|
||||
|
||||
// Font style options matching CSS font-style property
|
||||
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
||||
|
||||
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
||||
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||
|
||||
// Text decoration options
|
||||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
||||
|
||||
// Bitmask for tracking which properties have been explicitly set
|
||||
struct CssPropertyFlags {
|
||||
uint16_t alignment : 1;
|
||||
uint16_t fontStyle : 1;
|
||||
uint16_t fontWeight : 1;
|
||||
uint16_t decoration : 1;
|
||||
uint16_t indent : 1;
|
||||
uint16_t marginTop : 1;
|
||||
uint16_t marginBottom : 1;
|
||||
uint16_t paddingTop : 1;
|
||||
uint16_t paddingBottom : 1;
|
||||
uint16_t marginLeft : 1;
|
||||
uint16_t reserved : 6;
|
||||
|
||||
CssPropertyFlags()
|
||||
: alignment(0),
|
||||
fontStyle(0),
|
||||
fontWeight(0),
|
||||
decoration(0),
|
||||
indent(0),
|
||||
marginTop(0),
|
||||
marginBottom(0),
|
||||
paddingTop(0),
|
||||
paddingBottom(0),
|
||||
marginLeft(0),
|
||||
reserved(0) {}
|
||||
|
||||
[[nodiscard]] bool anySet() const {
|
||||
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || paddingTop ||
|
||||
paddingBottom || marginLeft;
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
alignment = fontStyle = fontWeight = decoration = indent = 0;
|
||||
marginTop = marginBottom = paddingTop = paddingBottom = marginLeft = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Represents a collection of CSS style properties
|
||||
// Only stores properties relevant to e-ink text rendering
|
||||
struct CssStyle {
|
||||
TextAlign alignment = TextAlign::None;
|
||||
CssFontStyle fontStyle = CssFontStyle::Normal;
|
||||
CssFontWeight fontWeight = CssFontWeight::Normal;
|
||||
CssTextDecoration decoration = CssTextDecoration::None;
|
||||
|
||||
float indentPixels = 0.0f; // First-line indent in pixels
|
||||
int8_t marginTop = 0; // Vertical spacing before block (in lines, 0-2)
|
||||
int8_t marginBottom = 0; // Vertical spacing after block (in lines, 0-2)
|
||||
int8_t paddingTop = 0; // Padding before (in lines, 0-2)
|
||||
int8_t paddingBottom = 0; // Padding after (in lines, 0-2)
|
||||
float marginLeft = 0.0f; // Horizontal indent in pixels (for blockquotes, etc.)
|
||||
|
||||
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.defined.alignment) {
|
||||
alignment = base.alignment;
|
||||
defined.alignment = 1;
|
||||
}
|
||||
if (base.defined.fontStyle) {
|
||||
fontStyle = base.fontStyle;
|
||||
defined.fontStyle = 1;
|
||||
}
|
||||
if (base.defined.fontWeight) {
|
||||
fontWeight = base.fontWeight;
|
||||
defined.fontWeight = 1;
|
||||
}
|
||||
if (base.defined.decoration) {
|
||||
decoration = base.decoration;
|
||||
defined.decoration = 1;
|
||||
}
|
||||
if (base.defined.indent) {
|
||||
indentPixels = base.indentPixels;
|
||||
defined.indent = 1;
|
||||
}
|
||||
if (base.defined.marginTop) {
|
||||
marginTop = base.marginTop;
|
||||
defined.marginTop = 1;
|
||||
}
|
||||
if (base.defined.marginBottom) {
|
||||
marginBottom = base.marginBottom;
|
||||
defined.marginBottom = 1;
|
||||
}
|
||||
if (base.defined.paddingTop) {
|
||||
paddingTop = base.paddingTop;
|
||||
defined.paddingTop = 1;
|
||||
}
|
||||
if (base.defined.paddingBottom) {
|
||||
paddingBottom = base.paddingBottom;
|
||||
defined.paddingBottom = 1;
|
||||
}
|
||||
if (base.defined.marginLeft) {
|
||||
marginLeft = base.marginLeft;
|
||||
defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatibility accessors for existing code that uses hasX pattern
|
||||
[[nodiscard]] bool hasTextAlign() const { return defined.alignment; }
|
||||
[[nodiscard]] bool hasFontStyle() const { return defined.fontStyle; }
|
||||
[[nodiscard]] bool hasFontWeight() const { return defined.fontWeight; }
|
||||
[[nodiscard]] bool hasTextDecoration() const { return defined.decoration; }
|
||||
[[nodiscard]] bool hasTextIndent() const { return defined.indent; }
|
||||
[[nodiscard]] bool hasMarginTop() const { return defined.marginTop; }
|
||||
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||||
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||||
|
||||
// Merge another style (alias for applyOver for compatibility)
|
||||
void merge(const CssStyle& other) { applyOver(other); }
|
||||
|
||||
void reset() {
|
||||
alignment = TextAlign::None;
|
||||
fontStyle = CssFontStyle::Normal;
|
||||
fontWeight = CssFontWeight::Normal;
|
||||
decoration = CssTextDecoration::None;
|
||||
indentPixels = 0.0f;
|
||||
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
||||
marginLeft = 0.0f;
|
||||
defined.clearAll();
|
||||
}
|
||||
};
|
||||
@ -4,29 +4,17 @@
|
||||
#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-fr.trie.h"
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 5>;
|
||||
using EntryArray = std::array<LanguageEntry, 1>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
{"german", "de", &germanHyphenator},
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
{"spanish", "es", &spanishHyphenator}}};
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,734 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t es_trie_data[] = {
|
||||
0x00, 0x00, 0x34, 0xFC, 0x01, 0x04, 0x16, 0x02, 0x0E, 0x0C, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x22, 0x0F, 0x2C, 0x0F,
|
||||
0x22, 0x0D, 0x2C, 0x0D, 0x0B, 0x16, 0x0B, 0x20, 0x15, 0x16, 0x15, 0x0C, 0x02, 0x0C, 0x17, 0x0E, 0x04, 0x2C, 0x05,
|
||||
0x04, 0x0D, 0x04, 0x21, 0x04, 0x18, 0x0D, 0x04, 0x17, 0x04, 0x0D, 0x17, 0x04, 0x0E, 0x0D, 0x04, 0x0D, 0x21, 0x04,
|
||||
0x0D, 0x21, 0x21, 0x0F, 0x0E, 0x0F, 0x0E, 0x0D, 0x0F, 0x0E, 0x17, 0x33, 0x33, 0x0C, 0x33, 0x16, 0x29, 0x29, 0x0C,
|
||||
0x29, 0x16, 0x21, 0x0C, 0x21, 0x0E, 0x34, 0x0D, 0x3E, 0x36, 0x0D, 0x3F, 0x2B, 0x16, 0x0D, 0x3D, 0x3D, 0x0C, 0x3D,
|
||||
0x16, 0x1F, 0x1F, 0x16, 0x2A, 0x2C, 0x0D, 0x0E, 0x0E, 0x21, 0x1F, 0x0C, 0x2A, 0x0D, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C,
|
||||
0x2A, 0x0B, 0x16, 0x37, 0x20, 0x0C, 0x20, 0x16, 0x35, 0x24, 0x47, 0x47, 0x0C, 0x47, 0x16, 0x20, 0x0B, 0x20, 0x0D,
|
||||
0x0C, 0x20, 0x0D, 0x16, 0x20, 0x20, 0x03, 0x17, 0x0E, 0x0D, 0x23, 0x0E, 0x17, 0x17, 0x17, 0x21, 0x16, 0x0D, 0x18,
|
||||
0x48, 0x49, 0x16, 0x0C, 0x0C, 0x16, 0x0C, 0x16, 0x2D, 0x2B, 0x0E, 0x0D, 0x2B, 0x0E, 0x17, 0x17, 0x2B, 0x34, 0x0B,
|
||||
0x34, 0x0B, 0x0C, 0x34, 0x0B, 0x16, 0x21, 0x20, 0x0D, 0x21, 0x0E, 0x17, 0x20, 0x0D, 0x04, 0x0F, 0x19, 0x0C, 0x0D,
|
||||
0x2E, 0x0F, 0x0E, 0x21, 0x17, 0x0E, 0x2D, 0x0E, 0x2B, 0x0E, 0x22, 0x17, 0x17, 0x0E, 0x22, 0x0D, 0x0E, 0x38, 0x19,
|
||||
0x18, 0x03, 0x0C, 0x22, 0x0B, 0x0E, 0x22, 0x0B, 0x18, 0x40, 0x2A, 0x0C, 0x0C, 0x2A, 0x0C, 0x16, 0x18, 0x0D, 0x0C,
|
||||
0x18, 0x0D, 0x0E, 0x2B, 0x21, 0x2B, 0x17, 0x2A, 0x16, 0x02, 0x33, 0x02, 0x33, 0x0C, 0x02, 0x33, 0x16, 0x35, 0x0E,
|
||||
0x04, 0x0C, 0x20, 0x0C, 0x0C, 0x20, 0x0C, 0x16, 0x2B, 0x0E, 0x0E, 0x2B, 0x0E, 0x18, 0x04, 0x0D, 0x0E, 0x0D, 0x19,
|
||||
0x0E, 0x41, 0x10, 0x2A, 0x20, 0x04, 0x0C, 0x0D, 0x03, 0x0E, 0x16, 0x0D, 0x0E, 0x18, 0x0F, 0x05, 0x0E, 0x07, 0x0E,
|
||||
0xA0, 0x00, 0x51, 0xA0, 0x00, 0x71, 0xA0, 0x00, 0xC3, 0xA3, 0x00, 0x71, 0x74, 0x6E, 0x7A, 0xFD, 0xFD, 0xFD, 0xA1,
|
||||
0x00, 0x71, 0x74, 0xF4, 0xA1, 0x00, 0x71, 0x6E, 0xEF, 0xA3, 0x00, 0x71, 0x74, 0x73, 0x6E, 0xEA, 0xEA, 0xEA, 0xA2,
|
||||
0x00, 0x71, 0x7A, 0x73, 0xE1, 0xE1, 0xA0, 0x00, 0xA2, 0xB6, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xD1, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
||||
0x01, 0xD2, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x05, 0xB1, 0xA0, 0x05, 0xC2, 0xA0, 0x05,
|
||||
0xE2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x05,
|
||||
0x81, 0xA0, 0x06, 0x72, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x05, 0x81, 0x6F, 0x61, 0xFA, 0xFD, 0xAE, 0x06,
|
||||
0x31, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x70, 0x71, 0x73, 0x74, 0x76, 0x7A, 0xED, 0xED, 0xF9, 0xED,
|
||||
0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0x21, 0x6E, 0xE1, 0xA0, 0x06, 0x01, 0xA0, 0x06, 0x92,
|
||||
0xA0, 0x06, 0x12, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x51, 0x21, 0x61,
|
||||
0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6F, 0xFD, 0x28, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0x6C, 0xDA, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xE3, 0xFD, 0x44, 0x75, 0x62, 0x65, 0x6F, 0xFF, 0x65, 0xFF,
|
||||
0x91, 0xFF, 0xC6, 0xFF, 0xEF, 0xA0, 0x04, 0x41, 0xA0, 0x04, 0x52, 0xA0, 0x04, 0x72, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xF5, 0x21, 0x61, 0xF1, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x69, 0x75,
|
||||
0xFE, 0xC5, 0xFE, 0xC8, 0xFE, 0xCE, 0xFE, 0xC8, 0xFE, 0xD7, 0xFE, 0xDC, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE,
|
||||
0xDC, 0xFE, 0xC8, 0xFE, 0xE1, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xEA, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8,
|
||||
0xFE, 0xC8, 0xFE, 0xF4, 0xFE, 0xF4, 0xFF, 0xC7, 0xFF, 0xFD, 0x41, 0x6C, 0xFF, 0x45, 0x21, 0x61, 0xFC, 0x21, 0x75,
|
||||
0xFD, 0x41, 0x72, 0xFF, 0x3B, 0x22, 0x6E, 0x75, 0xF9, 0xFC, 0x41, 0x78, 0xFF, 0x32, 0x41, 0x78, 0xFF, 0x34, 0x21,
|
||||
0xB3, 0xFC, 0x41, 0x6E, 0xFF, 0x27, 0xA0, 0x01, 0x52, 0x21, 0x64, 0xFD, 0xA0, 0x06, 0x43, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x65, 0xFA, 0x23, 0x6E, 0x70, 0x76, 0xF4, 0xFA, 0xFD, 0x21, 0x74, 0xEA, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFA, 0x21,
|
||||
0x69, 0xED, 0x21, 0x6C, 0xFD, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xEA, 0xF4, 0xF7, 0xFD, 0x21, 0x6E, 0xF7, 0x25, 0x61,
|
||||
0x6F, 0xC3, 0x75, 0x65, 0xBB, 0xC0, 0xC8, 0xCB, 0xFD, 0xA1, 0x00, 0x61, 0x69, 0xF5, 0xA0, 0x07, 0xB1, 0x21, 0x62,
|
||||
0xFD, 0xA0, 0x00, 0xF1, 0x21, 0x68, 0xFD, 0x22, 0x69, 0x6F, 0xFA, 0xFA, 0x21, 0x74, 0xF5, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x24, 0x63, 0x73, 0x72, 0x74, 0xEF, 0xF2, 0xFD, 0xEC, 0xA2, 0x06, 0x01, 0x69, 0x65, 0xE0, 0xF7, 0xA0,
|
||||
0x02, 0x91, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFA, 0x21, 0x65, 0xFD, 0x22, 0x65, 0x72, 0xF7, 0xFD, 0x21, 0x6E, 0xEF,
|
||||
0x41, 0x6C, 0xFE, 0x6F, 0x22, 0x65, 0x75, 0xF9, 0xFC, 0x21, 0x74, 0xE3, 0x21, 0x73, 0xFD, 0x21, 0xB3, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x41, 0x63, 0xFE, 0x5A, 0x21, 0x73, 0xFC, 0x22, 0x65, 0x69, 0xFD, 0xF9, 0x21, 0x64, 0xCB, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xD3, 0x21, 0x69, 0xFD, 0x21, 0x6F, 0xB9, 0x21, 0x74, 0xFD,
|
||||
0xA7, 0x07, 0x62, 0x63, 0x67, 0x70, 0x6C, 0x72, 0x78, 0x75, 0xBF, 0xCB, 0xD9, 0xE3, 0xF1, 0xF7, 0xFD, 0x42, 0x63,
|
||||
0x74, 0xFF, 0xA2, 0xFF, 0xA2, 0x41, 0x63, 0xFF, 0x9B, 0x22, 0x69, 0x75, 0xF5, 0xFC, 0x41, 0x69, 0xFF, 0x92, 0x21,
|
||||
0x63, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0xA1, 0xFE, 0x0B, 0x21, 0xC3, 0xFC, 0x41, 0x73, 0xFF, 0x81, 0x21, 0x69, 0xFC,
|
||||
0xA4, 0x07, 0x62, 0x64, 0x66, 0x74, 0x78, 0xE3, 0xEF, 0xF6, 0xFD, 0x41, 0x75, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41,
|
||||
0x6F, 0xFD, 0xEB, 0xA2, 0x07, 0x62, 0x6D, 0x74, 0xF9, 0xFC, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32,
|
||||
0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x43, 0x65, 0xC3, 0x74, 0xFF, 0xFA, 0xFF, 0xFD, 0xFF, 0x2A, 0x41, 0x6E, 0xFF,
|
||||
0x20, 0x21, 0xAD, 0xFC, 0x23, 0x65, 0x69, 0xC3, 0xF9, 0xF9, 0xFD, 0x21, 0x64, 0xF9, 0xA3, 0x05, 0x02, 0x6B, 0x70,
|
||||
0x72, 0xD9, 0xE5, 0xFD, 0xA0, 0x07, 0x62, 0xA0, 0x07, 0xA1, 0x21, 0x6C, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x07, 0x82,
|
||||
0x67, 0xFD, 0xA0, 0x07, 0x82, 0xC1, 0x07, 0x82, 0x70, 0xFE, 0xFD, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF2, 0xF7,
|
||||
0xF7, 0xFA, 0xF7, 0xA0, 0x01, 0xA1, 0x21, 0x62, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x48, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0x6E, 0xFE, 0xF2, 0xFF, 0x46, 0xFF, 0x7F, 0xFF, 0x95, 0xFF, 0xC6, 0xFF, 0xCF, 0xFF, 0xE9,
|
||||
0xFF, 0xFD, 0xA0, 0x0A, 0x01, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0xA2, 0x00, 0x61, 0x6F, 0x61, 0xDE, 0xFD, 0xA0,
|
||||
0x08, 0x12, 0xA0, 0x08, 0x33, 0xC2, 0x07, 0x82, 0x6D, 0x6E, 0xFD, 0x4D, 0xFD, 0x4D, 0xA0, 0x0B, 0x45, 0x23, 0xA1,
|
||||
0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6, 0xF9, 0x21, 0x73, 0xF7, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA1, 0x07, 0x82, 0x6E, 0xFD, 0xA0, 0x08, 0x63, 0xA0,
|
||||
0x08, 0x92, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFA, 0xFD, 0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xFF, 0xB9, 0xFF, 0xBC, 0xFF, 0xBF, 0xFF, 0xEA, 0xFF, 0x70, 0xFF, 0x70, 0xFF, 0xF5, 0x42, 0x73, 0x75,
|
||||
0xFF, 0xEA, 0xFF, 0x96, 0xA0, 0x09, 0x91, 0x21, 0x68, 0xFD, 0xA1, 0x09, 0x81, 0x63, 0xFD, 0x21, 0x6F, 0xFB, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0x61, 0x65, 0x69, 0xE2, 0xFD, 0xA0, 0x00, 0x61, 0xA0,
|
||||
0x0C, 0xC3, 0x21, 0x75, 0xFD, 0xA0, 0x04, 0x91, 0x21, 0x74, 0xFD, 0x22, 0x64, 0x73, 0xF7, 0xFD, 0x22, 0x6E, 0x73,
|
||||
0xF5, 0xF5, 0x21, 0x6F, 0xFB, 0x22, 0x74, 0x7A, 0xED, 0xED, 0x21, 0x6E, 0xFB, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0x43, 0x63, 0x65, 0x6E, 0xFF, 0xEF, 0xFD, 0x20, 0xFF, 0xFD, 0x21, 0x6E, 0xD8, 0x23, 0x65, 0x61, 0x69, 0xD8, 0xF3,
|
||||
0xFD, 0x21, 0x6C, 0xF9, 0x41, 0x69, 0xFD, 0x1D, 0xA0, 0x04, 0xA2, 0xA0, 0x04, 0xC2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xB3, 0xEF, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xF5, 0x22, 0x6C, 0x6F, 0xDC, 0xF1, 0xA2, 0x00, 0x61, 0x61, 0x69, 0xD4, 0xFB, 0xA0, 0x0D, 0x43, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62, 0xF4, 0x21, 0xA1, 0xFD, 0x22, 0xC3, 0x61, 0xFD, 0xFA, 0x23,
|
||||
0x66, 0x6D, 0x72, 0xEF, 0xF2, 0xFB, 0xA0, 0x0D, 0x73, 0x21, 0x62, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA0,
|
||||
0x0D, 0x42, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x70, 0xFD, 0x22, 0xA1, 0xB3, 0xF1, 0xFD, 0x21, 0x70, 0xEF,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x6D, 0xE3, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xFA,
|
||||
0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFE, 0x28, 0x21, 0x73, 0xFC, 0x21, 0x6C, 0xCB, 0x22, 0x69,
|
||||
0x65, 0xFA, 0xFD, 0x45, 0x61, 0xC3, 0x65, 0x69, 0x68, 0xFF, 0xB0, 0xFF, 0xCF, 0xFF, 0xDD, 0xFF, 0xEE, 0xFF, 0xFB,
|
||||
0x21, 0x6E, 0xF0, 0xA0, 0x06, 0xD2, 0x41, 0x69, 0xFB, 0xE3, 0xA0, 0x0E, 0x92, 0x21, 0x65, 0xFD, 0xC3, 0x0D, 0xB3,
|
||||
0x63, 0x72, 0x6A, 0xFF, 0xF6, 0xFB, 0xD9, 0xFF, 0xFD, 0x21, 0x6F, 0xEE, 0x21, 0xAD, 0xFD, 0x43, 0x67, 0x69, 0xC3,
|
||||
0xFB, 0xC7, 0xFF, 0xE8, 0xFF, 0xFD, 0xA0, 0x06, 0xB2, 0xA1, 0x0E, 0x92, 0x61, 0xDB, 0x22, 0x63, 0x72, 0xF8, 0xFB,
|
||||
0x21, 0x65, 0xFB, 0x41, 0x72, 0xFB, 0xAD, 0xA1, 0x0E, 0x92, 0x73, 0xCA, 0x23, 0x65, 0x61, 0x69, 0xC5, 0xFB, 0xC5,
|
||||
0x21, 0x61, 0xBE, 0xC6, 0x0D, 0xB3, 0x72, 0x6C, 0x61, 0x6D, 0x74, 0x6F, 0xFF, 0xD3, 0xFF, 0xEA, 0xFF, 0xED, 0xFF,
|
||||
0xF6, 0xFF, 0xFD, 0xFB, 0x9A, 0xA0, 0x05, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0xC3, 0x05, 0x81, 0x64, 0x65,
|
||||
0x75, 0xFC, 0x8E, 0xFF, 0x9D, 0xFF, 0xFD, 0xC1, 0x0E, 0x92, 0x6F, 0xFF, 0x91, 0x43, 0xA1, 0xA9, 0xB3, 0xFF, 0x8B,
|
||||
0xFF, 0x8B, 0xFF, 0x8B, 0x45, 0x61, 0x65, 0x6C, 0x6F, 0xC3, 0xFF, 0x81, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, 0x81, 0xFF,
|
||||
0xF6, 0x42, 0x61, 0x6F, 0xFF, 0x71, 0xFF, 0x71, 0x41, 0x72, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41, 0x72, 0xFF, 0x63,
|
||||
0x21, 0x65, 0xFC, 0x41, 0x61, 0xFB, 0x3B, 0x42, 0x6D, 0x74, 0xFB, 0x37, 0xFF, 0xFC, 0xC7, 0x0D, 0xB3, 0x6E, 0x67,
|
||||
0x6C, 0x7A, 0x6D, 0x63, 0x73, 0xFF, 0xB4, 0xFF, 0x63, 0xFF, 0xD0, 0xFF, 0xE0, 0xFF, 0xEB, 0xFF, 0xF2, 0xFF, 0xF9,
|
||||
0x41, 0x65, 0xFF, 0x5B, 0x41, 0x73, 0xFF, 0x8F, 0x21, 0x65, 0xFC, 0xA2, 0x0D, 0xB3, 0x70, 0x72, 0xF5, 0xFD, 0x42,
|
||||
0x61, 0x65, 0xFF, 0x27, 0xFF, 0x39, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x20, 0xFF, 0x95, 0xFF, 0x20, 0xFF, 0x20,
|
||||
0xA2, 0x0D, 0xB3, 0x72, 0x6C, 0xEC, 0xF3, 0xA0, 0x0D, 0xE3, 0xC1, 0x0D, 0xE3, 0x6E, 0xFA, 0xE8, 0xA0, 0x0E, 0x72,
|
||||
0xA1, 0x05, 0x81, 0x69, 0xFD, 0xA1, 0x0D, 0xE3, 0x6E, 0xFB, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA, 0xEA, 0xED,
|
||||
0xFB, 0xEA, 0x41, 0x76, 0xFF, 0x0D, 0x41, 0x6D, 0xFF, 0x09, 0x22, 0x65, 0x6F, 0xF8, 0xFC, 0x48, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xD7, 0xFE, 0xE4, 0xFF, 0x23, 0xFF, 0x8D, 0xFF, 0xB0, 0xFF, 0xCB, 0xFF, 0xE8,
|
||||
0xFF, 0xFB, 0x21, 0x74, 0xE7, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFB, 0xB0, 0x21, 0xBA, 0xFC, 0x22, 0x75, 0xC3, 0xF9,
|
||||
0xFD, 0xA0, 0x01, 0x11, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x21, 0x64, 0xFB, 0xA2,
|
||||
0x04, 0xA2, 0x63, 0x72, 0xEA, 0xFD, 0x21, 0x6C, 0xE8, 0x21, 0x75, 0xFD, 0x21, 0x62, 0xFD, 0xA1, 0x04, 0xC2, 0x6D,
|
||||
0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD, 0xE3, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFD, 0x94, 0xFD, 0xD0, 0xFD, 0xD0, 0xFD, 0xD0, 0xFF, 0xDB, 0xFD, 0xD0, 0xFF,
|
||||
0xF0, 0x22, 0xA1, 0xAD, 0xB4, 0xB4, 0x24, 0x61, 0xC3, 0x65, 0x69, 0xAF, 0xFB, 0xAF, 0xAF, 0x21, 0x62, 0xF7, 0xC2,
|
||||
0x01, 0x11, 0x61, 0x6F, 0xFF, 0xA3, 0xFF, 0xA3, 0x21, 0x62, 0xF7, 0x21, 0xAD, 0xFD, 0xA2, 0x04, 0x91, 0x69, 0xC3,
|
||||
0xEE, 0xFD, 0x41, 0x74, 0xFA, 0x1F, 0x21, 0x72, 0xFC, 0x21, 0x6F, 0xFD, 0xA1, 0x0D, 0xB2, 0x62, 0xFD, 0x41, 0x72,
|
||||
0xFE, 0x63, 0x21, 0x61, 0xFC, 0xA1, 0x0D, 0xB2, 0x74, 0xFD, 0xA0, 0x0D, 0xB2, 0xA0, 0x0E, 0xB2, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xDE, 0xEA,
|
||||
0xEF, 0xEF, 0xEF, 0xF5, 0x42, 0x65, 0x6F, 0xFF, 0x88, 0xFF, 0xF1, 0xC3, 0x00, 0x61, 0x61, 0x6F, 0x72, 0xFD, 0xF4,
|
||||
0xFF, 0x3C, 0xFF, 0xF9, 0x41, 0x72, 0xFB, 0x6B, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xFB, 0x4A, 0x42, 0x6F, 0xC3, 0xFB,
|
||||
0x46, 0xFF, 0xFC, 0x43, 0x72, 0x69, 0x73, 0xFB, 0x3C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42, 0x73, 0x74, 0xFB, 0x32, 0xFB,
|
||||
0x32, 0x41, 0xAD, 0xFB, 0x48, 0x22, 0x69, 0xC3, 0xF5, 0xFC, 0x21, 0x6D, 0xFB, 0x41, 0x6D, 0xFB, 0x1F, 0x21, 0x72,
|
||||
0xFC, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x41, 0x76, 0xFB, 0x10, 0x21, 0xA1, 0xFC, 0xA0, 0x04, 0xE2,
|
||||
0x21, 0x70, 0xFD, 0x23, 0x61, 0xC3, 0x75, 0xF3, 0xF7, 0xFD, 0x21, 0x72, 0xF9, 0x41, 0x69, 0xFB, 0x5E, 0x21, 0x64,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x41, 0xB1, 0xFA, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xBA, 0xFD, 0x23, 0x6F, 0x75, 0xC3, 0xF3,
|
||||
0xFA, 0xFD, 0x41, 0x73, 0xFF, 0x42, 0x21, 0xBA, 0xFC, 0x42, 0x75, 0xC3, 0xFA, 0xF7, 0xFF, 0xFD, 0x41, 0x67, 0xFA,
|
||||
0xD3, 0x41, 0x7A, 0xFE, 0x14, 0x41, 0x6A, 0xFA, 0xC8, 0x23, 0xA9, 0xAD, 0xB3, 0xF4, 0xF8, 0xFC, 0x42, 0x61, 0xC3,
|
||||
0xF9, 0x40, 0xFB, 0x35, 0x42, 0x6D, 0x74, 0xF9, 0x39, 0xF9, 0x39, 0x43, 0x7A, 0x6D, 0x73, 0xFF, 0xF2, 0xFA, 0xAF,
|
||||
0xFF, 0xF9, 0x45, 0x65, 0xC3, 0x69, 0x6F, 0x71, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xF6, 0xFF, 0xDD, 0xFA, 0xA5, 0x41,
|
||||
0xAD, 0xFF, 0x76, 0x42, 0x69, 0xC3, 0xFF, 0x72, 0xFF, 0xFC, 0x43, 0xA1, 0xA9, 0xB3, 0xFA, 0x8A, 0xFA, 0x8A, 0xFA,
|
||||
0x8A, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFA, 0x80, 0xFF, 0xF6, 0xFA, 0x80, 0xFA, 0x80, 0x41, 0x65, 0xFA, 0xD8, 0x21,
|
||||
0x72, 0xFC, 0x42, 0x6E, 0x74, 0xFA, 0xA1, 0xFA, 0x6C, 0xA0, 0x00, 0x40, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||
0xA9, 0xAD, 0xFA, 0x94, 0xFF, 0xFD, 0x22, 0x65, 0xC3, 0xE9, 0xF9, 0x22, 0x61, 0x72, 0xE1, 0xFB, 0x41, 0x67, 0xFF,
|
||||
0x42, 0x41, 0xBA, 0xFF, 0x28, 0x42, 0x75, 0xC3, 0xFF, 0x24, 0xFF, 0xFC, 0xCE, 0x07, 0x62, 0x62, 0x64, 0x66, 0x67,
|
||||
0x63, 0x6A, 0x6C, 0x6E, 0x6D, 0x70, 0x65, 0x71, 0x7A, 0x73, 0xFF, 0x00, 0xFF, 0x1A, 0xFF, 0x27, 0xFF, 0x40, 0xFF,
|
||||
0x57, 0xFF, 0x65, 0xFF, 0x97, 0xFF, 0xAB, 0xFF, 0xBC, 0xFF, 0xEC, 0xFF, 0xF1, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0xF9,
|
||||
0xA0, 0x05, 0x02, 0x49, 0x6F, 0x63, 0x69, 0x66, 0x67, 0x76, 0x61, 0x73, 0x74, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x71,
|
||||
0xFA, 0x0C, 0xFA, 0x0C, 0xFA, 0x0C, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x0C, 0x41, 0x64, 0xF8, 0x73, 0x21, 0x6E, 0xFC,
|
||||
0x21, 0x69, 0xFD, 0xC3, 0x07, 0x62, 0x6E, 0x6D, 0x76, 0xFF, 0xDA, 0xFE, 0xDD, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0xF7,
|
||||
0x21, 0x65, 0xFC, 0x41, 0x61, 0xF9, 0xD3, 0x22, 0x69, 0x67, 0xF9, 0xFC, 0xC4, 0x07, 0x62, 0x62, 0x72, 0x63, 0x6A,
|
||||
0xFE, 0xC1, 0xFF, 0xFB, 0xF9, 0xCA, 0xFA, 0x73, 0x42, 0xA1, 0xB3, 0xF9, 0xBB, 0xF9, 0xBB, 0x43, 0x61, 0xC3, 0x6F,
|
||||
0xF9, 0xB4, 0xFF, 0xF9, 0xF9, 0xB4, 0x42, 0x63, 0x71, 0xFF, 0xF6, 0xF9, 0xAA, 0x42, 0x63, 0x71, 0xFF, 0xD0, 0xF9,
|
||||
0xA3, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xEF, 0xFD, 0xC1, 0x05, 0x81, 0x74, 0xFC, 0x34, 0x41, 0x74, 0xFC, 0x2E,
|
||||
0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x42, 0xA9, 0xB3, 0xF8, 0x05, 0xF8, 0x05, 0x48, 0x72, 0x61, 0x73,
|
||||
0x6D, 0x65, 0xC3, 0x64, 0x66, 0xF7, 0xFE, 0xF7, 0xFE, 0xF7, 0xFE, 0xFF, 0x16, 0xF7, 0xFE, 0xFF, 0xF9, 0xF7, 0xFE,
|
||||
0xF9, 0x7B, 0xC1, 0x05, 0x81, 0x72, 0xF7, 0xE5, 0x42, 0xAD, 0xA1, 0xFF, 0xFA, 0xF7, 0xDF, 0x43, 0x69, 0xC3, 0x74,
|
||||
0xFF, 0xDA, 0xFF, 0xF9, 0xF9, 0x55, 0x41, 0xA1, 0xF9, 0x4E, 0x42, 0x61, 0xC3, 0xF9, 0x4A, 0xFF, 0xFC, 0x41, 0x7A,
|
||||
0xF9, 0x40, 0x21, 0xAD, 0xFC, 0x22, 0x69, 0xC3, 0xF9, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x69, 0xFD, 0xC5, 0x07, 0x62,
|
||||
0x62, 0x6D, 0x6E, 0x73, 0x74, 0xFF, 0x95, 0xFF, 0xA7, 0xFF, 0xD9, 0xFF, 0xE7, 0xFF, 0xFD, 0x43, 0x61, 0x65, 0x6F,
|
||||
0xF9, 0x1C, 0xF9, 0x1C, 0xF9, 0x1C, 0xC2, 0x07, 0x82, 0x62, 0x6D, 0xF9, 0x15, 0xFF, 0xF6, 0x45, 0xA1, 0xA9, 0xAD,
|
||||
0xB3, 0xBA, 0xFF, 0xF7, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9, 0xF0, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3,
|
||||
0xFE, 0xBD, 0xFE, 0xEA, 0xFF, 0x13, 0xFF, 0x2F, 0xFF, 0xCB, 0xFF, 0xF0, 0xA1, 0x00, 0x61, 0x65, 0xED, 0x43, 0x6E,
|
||||
0x72, 0x73, 0xFE, 0xD2, 0xF8, 0xE1, 0xF8, 0xE1, 0xA0, 0x0C, 0x12, 0xA1, 0x0C, 0x12, 0x72, 0xFD, 0x21, 0x6E, 0xF8,
|
||||
0x21, 0xB3, 0xFD, 0x23, 0x61, 0x6F, 0xC3, 0xF2, 0xF5, 0xFD, 0x41, 0x70, 0xF7, 0x45, 0x41, 0x6E, 0xF7, 0x41, 0x21,
|
||||
0x65, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x74, 0xEC, 0xFD, 0x41, 0xA9, 0xF8,
|
||||
0xBA, 0x21, 0x65, 0xD6, 0x21, 0x69, 0xFD, 0xC7, 0x0F, 0x93, 0x65, 0x6C, 0x64, 0x6E, 0x72, 0xC3, 0x6D, 0xFF, 0xBE,
|
||||
0xF9, 0x7B, 0xFF, 0xD6, 0xFF, 0xF1, 0xF8, 0x9F, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0xA1, 0xFC, 0xEB, 0x21, 0xC3, 0xFC,
|
||||
0x42, 0x70, 0x74, 0xF8, 0xA9, 0xF7, 0x03, 0x22, 0x75, 0x65, 0xF6, 0xF9, 0x41, 0xA1, 0xF8, 0x74, 0x44, 0x61, 0xC3,
|
||||
0x65, 0x6F, 0xF8, 0x70, 0xFF, 0xFC, 0xF8, 0x70, 0xF8, 0x70, 0x21, 0x74, 0xF3, 0x41, 0x65, 0xF6, 0xE3, 0x21, 0x75,
|
||||
0xFC, 0x21, 0x6C, 0xFD, 0x41, 0x61, 0xFA, 0xF6, 0x41, 0x6E, 0xFC, 0xB6, 0x21, 0x65, 0xFC, 0x21, 0x6D, 0xFD, 0x41,
|
||||
0x65, 0xF8, 0x4B, 0x23, 0x63, 0x69, 0x74, 0xEE, 0xF9, 0xFC, 0x41, 0x69, 0xF8, 0x66, 0x21, 0x6D, 0xFC, 0x21, 0xB3,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0xC6, 0x0F, 0x93, 0x63, 0x73, 0x66, 0x6C, 0x72, 0x74, 0xFF, 0xB7, 0xFF, 0xCD, 0xFF, 0xD7,
|
||||
0xFF, 0xEC, 0xFB, 0x06, 0xFF, 0xFD, 0x41, 0x75, 0xFC, 0x7F, 0x21, 0x63, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x6D, 0xFF,
|
||||
0x57, 0x21, 0x65, 0xFC, 0x21, 0x6C, 0xAA, 0x21, 0x70, 0xFD, 0x41, 0x74, 0xFF, 0x4A, 0x41, 0x65, 0xF8, 0x29, 0x41,
|
||||
0x6D, 0xF6, 0x7F, 0x21, 0xAD, 0xFC, 0x41, 0x75, 0xF8, 0x1E, 0x44, 0x61, 0x69, 0xC3, 0x72, 0xF8, 0x1A, 0xFF, 0xF5,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0x22, 0x70, 0x74, 0xE4, 0xF3, 0xA5, 0x0F, 0x93, 0x6A, 0x6C, 0x6D, 0x6E, 0x73, 0xCB, 0xD2,
|
||||
0xD8, 0xDB, 0xFB, 0x41, 0x69, 0xFC, 0x36, 0x21, 0x70, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x63, 0xFA,
|
||||
0x65, 0x21, 0x69, 0xFC, 0x41, 0xAD, 0xF7, 0xCF, 0x42, 0x69, 0xC3, 0xF7, 0xCB, 0xFF, 0xFC, 0x21, 0x64, 0xF9, 0xA3,
|
||||
0x0F, 0x93, 0x63, 0x66, 0x72, 0xE8, 0xEF, 0xFD, 0x41, 0x62, 0xFA, 0xEF, 0xA1, 0x0F, 0x93, 0x72, 0xFC, 0x42, 0xA9,
|
||||
0xB3, 0xF7, 0x9E, 0xF7, 0x9E, 0x42, 0x61, 0xC3, 0xF7, 0x97, 0xFF, 0xF9, 0x21, 0x74, 0xF9, 0x41, 0x74, 0xFF, 0x50,
|
||||
0xA2, 0x0F, 0xC3, 0x73, 0x72, 0xF9, 0xFC, 0xA0, 0x0F, 0xC3, 0xC3, 0x0F, 0xC3, 0x6E, 0x6D, 0x72, 0xFD, 0x8F, 0xFA,
|
||||
0x1F, 0xF7, 0x7F, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA, 0xF1, 0xF4, 0xF1, 0xF1, 0x41, 0x79, 0xF8, 0x11, 0x21,
|
||||
0x61, 0xFC, 0x48, 0x69, 0x68, 0x61, 0x65, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xC2, 0xF8, 0x91, 0xFF, 0x31, 0xFF, 0x82,
|
||||
0xFF, 0xB1, 0xFF, 0xBE, 0xFF, 0xEE, 0xFF, 0xFD, 0x41, 0x74, 0xF8, 0x78, 0x21, 0x73, 0xFC, 0x41, 0x73, 0xF8, 0x71,
|
||||
0x21, 0x65, 0xFC, 0x22, 0x65, 0x6F, 0xF6, 0xFD, 0x22, 0x62, 0x72, 0xD4, 0xFB, 0x41, 0x73, 0xFD, 0x21, 0x21, 0x61,
|
||||
0xFC, 0x41, 0x61, 0xFD, 0x39, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62,
|
||||
0xFD, 0xA3, 0x00, 0x61, 0x75, 0x6F, 0x65, 0xE1, 0xEA, 0xFD, 0x41, 0x70, 0xF6, 0x09, 0x21, 0x6D, 0xFC, 0x41, 0x6A,
|
||||
0xF6, 0x02, 0xA0, 0x05, 0x52, 0x21, 0x74, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0x22, 0x62, 0x6C, 0xF0, 0xFD,
|
||||
0x22, 0x69, 0x6F, 0xE8, 0xFB, 0x21, 0x65, 0xFB, 0x21, 0x6C, 0xFD, 0xC1, 0x0E, 0xB2, 0x67, 0xF9, 0x8A, 0x41, 0x67,
|
||||
0xF5, 0x63, 0xA1, 0x0E, 0xB2, 0x65, 0xFC, 0x41, 0xB1, 0xF9, 0x7B, 0xA1, 0x0E, 0xB2, 0xC3, 0xFC, 0x43, 0xA1, 0xA9,
|
||||
0xB3, 0xF5, 0x51, 0xF5, 0x51, 0xF5, 0x51, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xF5, 0x47, 0xFF, 0xF6, 0xF5, 0x47, 0xF5,
|
||||
0x47, 0x21, 0x74, 0xF3, 0xC2, 0x0E, 0xB2, 0x64, 0x6E, 0xFA, 0x38, 0xFF, 0xFD, 0xA0, 0x10, 0xD2, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF9, 0x3A, 0xFB,
|
||||
0x1F, 0xFF, 0xB7, 0xFF, 0xC1, 0xFF, 0xCA, 0xFF, 0xE9, 0xFF, 0xF5, 0x21, 0x73, 0xEA, 0x41, 0x78, 0xF8, 0x7E, 0x21,
|
||||
0xB3, 0xFC, 0x21, 0xC3, 0xFD, 0x22, 0x61, 0x69, 0xF3, 0xFD, 0xC2, 0x00, 0x61, 0x65, 0x72, 0xFF, 0x8C, 0xFF, 0xFB,
|
||||
0x42, 0x61, 0x6F, 0xF6, 0x48, 0xF6, 0x48, 0x21, 0x65, 0xF9, 0x21, 0x6D, 0xFD, 0x41, 0x65, 0xF6, 0x3B, 0x21, 0x65,
|
||||
0xFC, 0x21, 0x6D, 0xFD, 0x22, 0x75, 0x65, 0xF3, 0xFD, 0x41, 0x65, 0xF5, 0x60, 0x21, 0x72, 0xFC, 0x41, 0x6F, 0xF5,
|
||||
0x59, 0x21, 0x72, 0xFC, 0x41, 0x72, 0xFB, 0x39, 0x21, 0x67, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x68,
|
||||
0xFB, 0x2C, 0x21, 0x6F, 0xFC, 0x21, 0x63, 0xFD, 0x41, 0x69, 0xF6, 0x72, 0x21, 0x6E, 0xFC, 0x41, 0x72, 0xF6, 0x6B,
|
||||
0x23, 0x6C, 0x6D, 0x65, 0xF2, 0xF9, 0xFC, 0x41, 0xB3, 0xFC, 0x0A, 0x42, 0x6F, 0xC3, 0xFC, 0x06, 0xFF, 0xFC, 0x21,
|
||||
0x73, 0xF9, 0xA0, 0x05, 0x22, 0x21, 0x65, 0xFD, 0x42, 0x6A, 0x6E, 0xFF, 0xFD, 0xF9, 0x78, 0x21, 0x6F, 0xF9, 0x42,
|
||||
0x65, 0x69, 0xFF, 0xFD, 0xF5, 0x0B, 0x24, 0x65, 0x61, 0x69, 0x74, 0xBC, 0xD4, 0xE6, 0xF9, 0x41, 0x74, 0xFF, 0xA2,
|
||||
0xC4, 0x02, 0xB1, 0x62, 0x63, 0x6E, 0x74, 0xFF, 0x9B, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFC, 0x41, 0x74, 0xF6, 0xD3,
|
||||
0x21, 0x73, 0xFC, 0xA1, 0x01, 0x82, 0x65, 0xFD, 0x42, 0x6F, 0xC3, 0xF8, 0xA2, 0xFA, 0x85, 0x41, 0x69, 0xF5, 0xE2,
|
||||
0x21, 0x65, 0xFC, 0x41, 0xA9, 0xFD, 0x00, 0x42, 0x65, 0xC3, 0xFC, 0xFC, 0xFF, 0xFC, 0x41, 0x62, 0xF5, 0xB3, 0xA4,
|
||||
0x09, 0xA3, 0x6D, 0x63, 0x6A, 0x72, 0xE3, 0xEE, 0xF5, 0xFC, 0x41, 0xAD, 0xFA, 0xC6, 0x42, 0x69, 0xC3, 0xFA, 0xC2,
|
||||
0xFF, 0xFC, 0xA1, 0x09, 0xA3, 0x6D, 0xF9, 0xA0, 0x09, 0xA3, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFC, 0x2F, 0xFE, 0xC3,
|
||||
0xF4, 0x14, 0xF4, 0x14, 0xA1, 0x09, 0xA3, 0x6A, 0xF3, 0x43, 0x61, 0xC3, 0x65, 0xF4, 0x02, 0xF5, 0xF7, 0xF4, 0x02,
|
||||
0x21, 0x72, 0xF6, 0x21, 0x65, 0xFD, 0xA1, 0x09, 0xA3, 0x6D, 0xFD, 0xA0, 0x09, 0xD3, 0xC1, 0x09, 0xD3, 0x6A, 0xF6,
|
||||
0x40, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF7, 0xF7, 0xF7, 0xFA, 0xF7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xC3, 0xFF, 0x85, 0xFF, 0xA7, 0xFF, 0xBD, 0xFF, 0xC2, 0xFF, 0xD2, 0xFF, 0xE7, 0xFF, 0xF5, 0x41, 0x73, 0xF6, 0x3B,
|
||||
0x42, 0x6C, 0x75, 0xF6, 0x37, 0xFF, 0xFC, 0x41, 0x6C, 0xF6, 0x30, 0x41, 0x72, 0xFF, 0x59, 0x41, 0x6D, 0xF6, 0x28,
|
||||
0x44, 0xA1, 0xAD, 0xB3, 0xBA, 0xFF, 0xF4, 0xF6, 0x27, 0xFF, 0xF8, 0xFF, 0xFC, 0xC5, 0x01, 0x82, 0x61, 0xC3, 0x69,
|
||||
0x6F, 0x75, 0xFF, 0xE0, 0xFF, 0xF3, 0xF6, 0x1A, 0xFF, 0xEB, 0xFF, 0xEF, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF,
|
||||
0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFF, 0xDE,
|
||||
0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0xF0, 0x42, 0x6E, 0x78, 0xFF, 0x8E, 0xFF, 0xEA,
|
||||
0xA0, 0x01, 0x82, 0x41, 0x72, 0xF5, 0x3F, 0x41, 0x72, 0xF5, 0x0B, 0x22, 0x61, 0x6F, 0xF8, 0xFC, 0x42, 0x6E, 0x70,
|
||||
0xF4, 0xEA, 0xF4, 0xEA, 0x21, 0x65, 0xF9, 0x41, 0x70, 0xF4, 0xE0, 0x22, 0x61, 0x6F, 0xFC, 0xFC, 0x41, 0x61, 0xFA,
|
||||
0xE0, 0x21, 0x75, 0xFC, 0x41, 0x6D, 0xFF, 0x00, 0x21, 0xA1, 0xFC, 0x41, 0x65, 0xF4, 0xBD, 0x22, 0xC3, 0x69, 0xF9,
|
||||
0xFC, 0x41, 0x69, 0xFB, 0x63, 0x21, 0x6C, 0xFC, 0x42, 0x6D, 0x63, 0xF4, 0x9C, 0xF3, 0x1F, 0x22, 0x61, 0x69, 0xF6,
|
||||
0xF9, 0x41, 0x61, 0xFE, 0xDD, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xF4, 0x89, 0x42, 0x61, 0x69, 0xFB, 0x45, 0xF4, 0xEA,
|
||||
0x43, 0x63, 0x68, 0x6E, 0xF4, 0xEC, 0xFF, 0xD2, 0xF4, 0xFD, 0x21, 0x65, 0xF6, 0x24, 0x61, 0x65, 0x6C, 0x72, 0xE5,
|
||||
0xE8, 0xEC, 0xFD, 0x41, 0x63, 0xF4, 0x85, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xF4, 0x72, 0x21, 0xC3, 0xFC, 0x41, 0x67,
|
||||
0xF4, 0x5A, 0x21, 0x75, 0xFC, 0x22, 0x6D, 0x72, 0xF6, 0xFD, 0x41, 0x69, 0xF4, 0x6E, 0x41, 0x62, 0xF2, 0xCD, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x76, 0xFD, 0x21, 0x6F, 0xFD, 0xCC, 0x09, 0xA3, 0x62, 0x63, 0x64, 0x67, 0x6C, 0x6E, 0x70, 0x66,
|
||||
0x72, 0x73, 0x74, 0x6D, 0xFF, 0x6B, 0xFF, 0x77, 0xFF, 0x7E, 0xFF, 0x87, 0xFF, 0x95, 0xFF, 0xA8, 0xFF, 0xCC, 0xFF,
|
||||
0xD9, 0xFF, 0xEA, 0xFF, 0xEF, 0xFA, 0x67, 0xFF, 0xFD, 0xC1, 0x02, 0x91, 0x69, 0xF4, 0x16, 0x21, 0x63, 0xFA, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x64, 0xF4, 0x78, 0x22, 0x75, 0x65, 0xFC, 0xAC, 0x41, 0x6F, 0xFA, 0x27, 0x42, 0x63, 0x61, 0xFF,
|
||||
0xFC, 0xF8, 0x70, 0x41, 0x76, 0xF2, 0x79, 0x21, 0xAD, 0xFC, 0x42, 0x69, 0xC3, 0xF4, 0x24, 0xFF, 0xFD, 0x21, 0x75,
|
||||
0xF9, 0xC2, 0x02, 0x91, 0x61, 0x68, 0xFF, 0x7D, 0xFA, 0x12, 0xC6, 0x09, 0xA3, 0x66, 0x6C, 0x6E, 0x71, 0x78, 0x76,
|
||||
0xFF, 0xCF, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xF7, 0xFE, 0x17, 0x42, 0x6F, 0x61, 0xF2, 0x4A, 0xF2, 0x4A,
|
||||
0x21, 0x75, 0xF9, 0x41, 0x6C, 0xFF, 0x2D, 0x21, 0x61, 0xFC, 0x21, 0x75, 0xFD, 0xC3, 0x09, 0xA3, 0x63, 0x67, 0x6E,
|
||||
0xFF, 0xF3, 0xFF, 0xFD, 0xF3, 0xB3, 0x41, 0x73, 0xFB, 0x5F, 0x44, 0x74, 0x61, 0xC3, 0x65, 0xF3, 0xA3, 0xF2, 0x26,
|
||||
0xF4, 0x1B, 0xF2, 0x26, 0x43, 0x6F, 0x61, 0x6C, 0xF2, 0x19, 0xF2, 0x19, 0xFF, 0xF3, 0x42, 0x63, 0x74, 0xF2, 0x0F,
|
||||
0xF2, 0x0F, 0x21, 0x6E, 0xF9, 0x22, 0x75, 0x65, 0xEC, 0xFD, 0x41, 0x73, 0xF2, 0x00, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0x41, 0x6F, 0xF8, 0x25, 0xA4, 0x09, 0xA3, 0x62, 0x63, 0x66, 0x70, 0xC8, 0xED, 0xF9, 0xFC, 0x41, 0x7A, 0xF1,
|
||||
0xE7, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x09, 0xA3, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x2B,
|
||||
0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3, 0x6E, 0xFD, 0x41, 0x74, 0xF4, 0x1F, 0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3, 0x64,
|
||||
0xFD, 0x41, 0x69, 0xF4, 0x16, 0xA1, 0x09, 0xD3, 0x74, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xE6, 0xFF,
|
||||
0xF2, 0xFD, 0xC7, 0xFD, 0xC7, 0xFF, 0xFB, 0xA0, 0x0C, 0x13, 0x21, 0x67, 0xFD, 0x22, 0x63, 0x74, 0xFA, 0xFA, 0x21,
|
||||
0x70, 0xF5, 0x22, 0x70, 0x6D, 0xF8, 0xFD, 0xA2, 0x05, 0x22, 0x6F, 0x75, 0xF0, 0xFB, 0xA0, 0x0A, 0x92, 0xA0, 0x0A,
|
||||
0xB3, 0xA0, 0x0B, 0x13, 0x23, 0xA1, 0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6,
|
||||
0xF9, 0xA1, 0x0A, 0xB3, 0x73, 0xF7, 0xA0, 0x0A, 0xE3, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0x28, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xD4, 0xD7, 0xED, 0xD7, 0xD7, 0xD7, 0xF5, 0x21,
|
||||
0x72, 0xEF, 0x21, 0x65, 0xFD, 0x48, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x74, 0xFD, 0xE7, 0xFE, 0x87, 0xFE,
|
||||
0xE8, 0xFF, 0x11, 0xFF, 0x55, 0xFF, 0x6D, 0xFF, 0x93, 0xFF, 0xFD, 0x21, 0x6E, 0xE7, 0x58, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x68, 0x61, 0x65,
|
||||
0x69, 0xF2, 0x79, 0xF3, 0xD1, 0xF4, 0x53, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0xC4,
|
||||
0xF4, 0x5A, 0xF7, 0x4E, 0xF4, 0x5A, 0xF9, 0xC2, 0xFB, 0x92, 0xFC, 0x33, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4,
|
||||
0x5A, 0xF4, 0x5A, 0xFC, 0x53, 0xFC, 0xC1, 0xFD, 0xC4, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x16, 0xA0, 0x0D, 0x22, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xC3, 0x00, 0x71, 0x2E, 0x69, 0x65, 0xF0, 0x3F, 0xFF, 0xF3, 0xFF, 0xFD, 0xC3, 0x00,
|
||||
0x71, 0x2E, 0x7A, 0x73, 0xF0, 0x33, 0xF0, 0x39, 0xF0, 0x39, 0xC1, 0x00, 0x71, 0x2E, 0xF0, 0x27, 0xD6, 0x00, 0x81,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xF0, 0x21, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24,
|
||||
0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0,
|
||||
0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0x41, 0x74, 0xF0, 0x69, 0x21, 0x70, 0xFC, 0xD7, 0x00, 0x91,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x65, 0xEF, 0xD5, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0,
|
||||
0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01,
|
||||
0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xFF, 0xFD, 0x42, 0x6F, 0x70, 0xF3, 0xA8, 0xFF, 0xB1,
|
||||
0x41, 0x6E, 0xFB, 0x50, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x69, 0x6F, 0xEF, 0x82, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF,
|
||||
0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE,
|
||||
0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xFF,
|
||||
0xF5, 0xFF, 0xFC, 0x41, 0x74, 0xF1, 0xF3, 0x21, 0x70, 0xFC, 0x41, 0x6F, 0xFC, 0x90, 0x21, 0x6C, 0xFC, 0x41, 0x74,
|
||||
0xF0, 0x5B, 0x41, 0x6D, 0xFA, 0xEF, 0x41, 0x61, 0xEF, 0x9F, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x25, 0x6D, 0x75,
|
||||
0x72, 0x73, 0x6E, 0xE4, 0xEB, 0xEE, 0xF2, 0xFD, 0xA0, 0x02, 0x32, 0x21, 0x61, 0xFD, 0x41, 0x2E, 0xEF, 0x06, 0xA1,
|
||||
0x02, 0x32, 0x73, 0xFC, 0x22, 0x6F, 0x61, 0xF1, 0xFB, 0x41, 0x64, 0xEF, 0x88, 0x23, 0x63, 0x67, 0x72, 0xEB, 0xF7,
|
||||
0xFC, 0x21, 0x6F, 0xE1, 0x41, 0x75, 0xEF, 0x68, 0x21, 0x72, 0xFC, 0x42, 0x64, 0x73, 0xFF, 0xFD, 0xF2, 0xE9, 0x22,
|
||||
0x6C, 0x61, 0xEF, 0xF9, 0x41, 0x6C, 0xEF, 0x64, 0x21, 0x61, 0xFC, 0xA0, 0x07, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0xA1, 0x04, 0x72, 0x72, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xEF, 0xD7, 0xEF, 0xD7, 0xEF,
|
||||
0xD7, 0xEF, 0xD7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEF, 0xC1, 0xEF, 0xC4, 0xEF, 0xC4, 0xEF, 0xC4,
|
||||
0xEF, 0xC4, 0xEF, 0xC4, 0xFF, 0xF0, 0x21, 0x69, 0xEA, 0x21, 0x74, 0xFD, 0x22, 0x66, 0x6E, 0xC3, 0xFD, 0x42, 0x68,
|
||||
0x6F, 0xF2, 0x5F, 0xEF, 0xB4, 0x21, 0x6E, 0xF9, 0xA0, 0x06, 0xF3, 0xA0, 0x07, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF3, 0x4F, 0xF3, 0x26,
|
||||
0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xF5, 0x21, 0x72, 0xE7, 0x21, 0x65, 0xFD, 0x41,
|
||||
0x6C, 0xFA, 0x21, 0x41, 0x6F, 0xF2, 0x6E, 0x24, 0x61, 0x62, 0x63, 0x74, 0xC5, 0xF5, 0xF8, 0xFC, 0x41, 0x6F, 0xEF,
|
||||
0x25, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0xDC, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x68, 0x6C, 0x72, 0x6F, 0x61, 0x75, 0x65, 0x69, 0xC3, 0xEE, 0x30, 0xEE, 0x33, 0xEE, 0x39, 0xEE,
|
||||
0x33, 0xEE, 0x42, 0xEE, 0x47, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x47, 0xFD, 0xF1, 0xEE, 0x4C, 0xEE, 0x33, 0xEE, 0x33,
|
||||
0xFD, 0xFD, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xFE, 0x09, 0xFE, 0x0F, 0xFE, 0x5B, 0xFE, 0xAE, 0xFF,
|
||||
0x19, 0xFF, 0x3C, 0xFF, 0x54, 0xFF, 0x9A, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x74, 0xF2, 0xB2, 0x21, 0x6E, 0xFC, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF1,
|
||||
0x95, 0xF1, 0xD1, 0xF1, 0xD1, 0xFF, 0xFB, 0xF1, 0xD1, 0xF1, 0xD1, 0xF1, 0xD7, 0x21, 0x61, 0xEA, 0xA0, 0x07, 0xC1,
|
||||
0xA0, 0x07, 0xD2, 0xA0, 0x07, 0xF2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x74, 0xFD,
|
||||
0x42, 0x61, 0x6F, 0xFF, 0xFD, 0xEE, 0xA8, 0x21, 0x6D, 0xF9, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0xA0, 0x09, 0x32, 0x21, 0x61, 0xFD, 0x22, 0x72, 0x6C, 0xF7, 0xFD, 0xA0, 0x02, 0x12, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xEF, 0x4A, 0x22, 0x68, 0x6C, 0xF9, 0xFC, 0x22, 0x61, 0x65, 0xE0, 0xE0, 0x21,
|
||||
0x68, 0xFB, 0x21, 0x74, 0xE3, 0x22, 0x63, 0x72, 0xFA, 0xFD, 0x23, 0xB3, 0xA1, 0xA9, 0xD6, 0xEB, 0xFB, 0x21, 0x6A,
|
||||
0xC0, 0x21, 0x6C, 0xBD, 0x21, 0x74, 0xBA, 0x21, 0x6E, 0xFD, 0x22, 0x6C, 0x65, 0xF7, 0xFD, 0xA0, 0x02, 0x11, 0x21,
|
||||
0x6A, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x69, 0xFF, 0xA6, 0x21, 0x6E, 0xFC, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0x9C,
|
||||
0x21, 0x61, 0xFC, 0x46, 0x64, 0x65, 0x69, 0x74, 0x67, 0x6E, 0xFF, 0x98, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xEC, 0xFF,
|
||||
0xF6, 0xFF, 0xFD, 0x42, 0x63, 0x7A, 0xFF, 0x82, 0xFF, 0x82, 0x41, 0x6E, 0xFF, 0x7B, 0x21, 0x65, 0xFC, 0x22, 0x65,
|
||||
0x69, 0xF2, 0xFD, 0x21, 0x64, 0xFB, 0x41, 0x67, 0xFF, 0x6C, 0x21, 0x69, 0xFC, 0x41, 0x72, 0xFF, 0x65, 0x21, 0x74,
|
||||
0xFC, 0x23, 0x65, 0x6C, 0x73, 0xEF, 0xF6, 0xFD, 0xA0, 0x09, 0x12, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFF, 0x51, 0x21,
|
||||
0xBA, 0xFC, 0x23, 0x61, 0x75, 0xC3, 0xF6, 0xF9, 0xFD, 0x21, 0x6F, 0xDE, 0xC2, 0x09, 0x12, 0x63, 0x64, 0xFF, 0x91,
|
||||
0xFF, 0x91, 0x23, 0xA1, 0xA9, 0xB3, 0xE0, 0xE0, 0xE0, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x6C, 0xFF, 0xF0, 0xFF, 0xF9,
|
||||
0xFF, 0xD9, 0xFF, 0xD9, 0xFF, 0x81, 0x41, 0x69, 0xFF, 0x84, 0x21, 0x72, 0xFC, 0x41, 0x65, 0xFF, 0x6A, 0x21, 0x63,
|
||||
0xFC, 0x42, 0xA1, 0xA9, 0xFF, 0x12, 0xFF, 0x12, 0x43, 0x61, 0xC3, 0x69, 0xFF, 0x0B, 0xFF, 0xF9, 0xFF, 0x0B, 0x41,
|
||||
0xA9, 0xFF, 0x01, 0x42, 0x65, 0xC3, 0xFE, 0xFD, 0xFF, 0xFC, 0x41, 0x67, 0xFF, 0x0A, 0x21, 0x65, 0xFC, 0x4B, 0x72,
|
||||
0x62, 0x63, 0x64, 0x6C, 0x70, 0x6E, 0x76, 0x78, 0x79, 0x73, 0xFF, 0x5A, 0xFF, 0x91, 0xFF, 0xA5, 0xFF, 0xAC, 0xFF,
|
||||
0xBF, 0xFF, 0xD3, 0xFF, 0xDA, 0xFF, 0xE4, 0xFF, 0x49, 0xFF, 0xF2, 0xFF, 0xFD, 0xA0, 0x08, 0xC3, 0x21, 0x64, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x72, 0xF7, 0x22, 0x72, 0x6F, 0xFA, 0xFD, 0x22, 0x7A, 0x63, 0xEF, 0xEF, 0x21, 0x69, 0xFB,
|
||||
0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0xA1, 0xA9, 0xB3, 0xDE, 0xDE, 0xDE, 0x22, 0x61, 0xC3,
|
||||
0xD7, 0xF9, 0x23, 0x61, 0x65, 0x6F, 0xD2, 0xD2, 0xD2, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xF1, 0xFD, 0xA0, 0x08,
|
||||
0xF2, 0x21, 0x73, 0xC0, 0x22, 0x61, 0x69, 0xFA, 0xFD, 0x21, 0x75, 0xFB, 0x21, 0xAD, 0xC6, 0x22, 0x69, 0xC3, 0xC3,
|
||||
0xFD, 0x42, 0x76, 0x6E, 0xFF, 0xAD, 0xFF, 0xFB, 0x42, 0x61, 0x69, 0xED, 0xDD, 0xFF, 0xF9, 0x41, 0x6C, 0xFE, 0x80,
|
||||
0x42, 0x72, 0x65, 0xFE, 0x7C, 0xFF, 0xFC, 0x21, 0x67, 0xF9, 0x41, 0x76, 0xFF, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x73,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x6C, 0xFF, 0x7E, 0x21, 0x6C, 0xFC, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x41, 0x72, 0xFE, 0x52, 0x43, 0x61, 0x65, 0x74, 0xF1, 0xB9, 0xF1, 0xB9, 0xFF, 0xFC, 0x41,
|
||||
0x73, 0xFF, 0xA0, 0x21, 0x65, 0xFC, 0x41, 0x6E, 0xFF, 0x5C, 0x21, 0x75, 0xFC, 0x21, 0xB3, 0xF9, 0x22, 0xC3, 0x6F,
|
||||
0xFD, 0xF6, 0x4D, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x79, 0x7A, 0xFF, 0x59, 0xFF,
|
||||
0x6C, 0xFF, 0x85, 0xFF, 0x95, 0xFE, 0x37, 0xFF, 0xA7, 0xFF, 0xB9, 0xFF, 0xCC, 0xFF, 0xD9, 0xFF, 0xE0, 0xFF, 0xEE,
|
||||
0xFF, 0xF5, 0xFF, 0xFB, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x25, 0xFF, 0x47, 0xFF, 0x25, 0xFF, 0x25, 0x21, 0x6A,
|
||||
0xF3, 0x41, 0x6A, 0xFF, 0x43, 0x21, 0xA9, 0xFC, 0x41, 0xB1, 0xFD, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xA9, 0xFD, 0x22,
|
||||
0x65, 0xC3, 0xFA, 0xFD, 0x23, 0x65, 0xC3, 0x70, 0xE7, 0xEE, 0xFB, 0x41, 0x6E, 0xFD, 0xD9, 0x21, 0xA9, 0xFC, 0x22,
|
||||
0x65, 0xC3, 0xF9, 0xFD, 0x21, 0x72, 0xFB, 0x21, 0x66, 0xFD, 0xC6, 0x02, 0x11, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73,
|
||||
0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0x46, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73,
|
||||
0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0x21, 0xA1, 0xED, 0xC6, 0x09, 0x12, 0x6E,
|
||||
0x72, 0x62, 0x64, 0x6D, 0x73, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0x42, 0xA1,
|
||||
0xB3, 0xFF, 0xEB, 0xFE, 0x1C, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFE, 0x15, 0xFE, 0x35, 0xFE, 0x15, 0xFE, 0x15, 0x44,
|
||||
0x6F, 0x61, 0xC3, 0x68, 0xFE, 0x08, 0xFF, 0xD7, 0xFF, 0xEC, 0xFF, 0xF3, 0x41, 0xA9, 0xFE, 0x85, 0x42, 0x65, 0xC3,
|
||||
0xFE, 0x81, 0xFF, 0xFC, 0xA1, 0x05, 0x92, 0x75, 0xF9, 0x41, 0x66, 0xFD, 0x42, 0x42, 0x63, 0x71, 0xFD, 0x3E, 0xFD,
|
||||
0x3E, 0x22, 0x69, 0x75, 0xF5, 0xF9, 0x41, 0x62, 0xFD, 0xCD, 0x21, 0x6D, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x7A, 0xFD,
|
||||
0x28, 0x42, 0x63, 0x6E, 0xFD, 0x75, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0x21, 0x72, 0xFD, 0x44, 0x61, 0x65, 0x69, 0x75,
|
||||
0xFD, 0x17, 0xFF, 0xFD, 0xFD, 0x9C, 0xFD, 0x7B, 0x41, 0x69, 0xFD, 0x4D, 0x41, 0x69, 0xFD, 0x8B, 0x43, 0x62, 0x63,
|
||||
0x6C, 0xFF, 0xF8, 0xFD, 0x5C, 0xFF, 0xFC, 0x41, 0x73, 0xFC, 0xF8, 0x41, 0x63, 0xFC, 0xF4, 0x22, 0x65, 0x75, 0xF8,
|
||||
0xFC, 0x43, 0x61, 0x69, 0x72, 0xFF, 0xE9, 0xFD, 0x4F, 0xFF, 0xFB, 0x23, 0x63, 0x70, 0x74, 0xB6, 0xCA, 0xF6, 0x42,
|
||||
0x63, 0x74, 0xFC, 0xF1, 0xFC, 0xEE, 0x4A, 0x6D, 0x6E, 0x6F, 0x61, 0xC3, 0x63, 0x71, 0x64, 0x73, 0x72, 0xFF, 0x07,
|
||||
0xFF, 0x1D, 0xFD, 0x24, 0xFF, 0x20, 0xFF, 0x48, 0xFF, 0x74, 0xFF, 0x8C, 0xFF, 0x9C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42,
|
||||
0x72, 0x6F, 0xFD, 0x05, 0xFC, 0xF7, 0x42, 0x61, 0x6F, 0xFC, 0xFE, 0xFC, 0xFE, 0x22, 0x65, 0x69, 0xF2, 0xF9, 0x41,
|
||||
0x74, 0xFC, 0xF2, 0x21, 0x72, 0xFC, 0x41, 0xA1, 0xFC, 0xDD, 0x42, 0x61, 0xC3, 0xFC, 0xD9, 0xFF, 0xFC, 0x42, 0x6E,
|
||||
0x75, 0xFC, 0xE0, 0xFF, 0xF9, 0x41, 0xB3, 0xFD, 0x0D, 0x42, 0x6F, 0xC3, 0xFD, 0x09, 0xFF, 0xFC, 0x21, 0x69, 0xF9,
|
||||
0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x42, 0x67, 0x6E, 0xFF, 0x6E, 0xFC, 0x74, 0x41, 0x65, 0xFF, 0x75, 0x42, 0x6F,
|
||||
0x72, 0xFC, 0xEE, 0xFF, 0xFC, 0x22, 0x61, 0x70, 0xEE, 0xF9, 0x41, 0x72, 0xFD, 0x0C, 0x41, 0x73, 0xFC, 0x9F, 0x21,
|
||||
0x75, 0xFC, 0x44, 0x65, 0x6C, 0x6F, 0x72, 0xFC, 0x9B, 0xFF, 0x4C, 0xFF, 0xF5, 0xFF, 0xFD, 0x42, 0x63, 0x74, 0xFC,
|
||||
0xEE, 0xFC, 0xEE, 0x21, 0x6E, 0xF9, 0x41, 0x63, 0xFC, 0x8C, 0x41, 0x72, 0xFC, 0x7D, 0xC1, 0x05, 0x92, 0x61, 0xFC,
|
||||
0x97, 0x41, 0x72, 0xFC, 0x91, 0x24, 0x65, 0x61, 0x6C, 0x6F, 0xEE, 0xF2, 0xF6, 0xFC, 0x41, 0x62, 0xFC, 0x20, 0x21,
|
||||
0x69, 0xFC, 0x41, 0x63, 0xFC, 0x5F, 0x41, 0x61, 0xFC, 0x58, 0x22, 0x65, 0x74, 0xF8, 0xFC, 0x42, 0x67, 0x72, 0xFD,
|
||||
0xCE, 0xFC, 0x20, 0x41, 0x78, 0xFC, 0x05, 0x23, 0x65, 0x6F, 0x75, 0xF5, 0xFC, 0xE1, 0x41, 0x65, 0xFC, 0x95, 0x47,
|
||||
0x63, 0x65, 0x66, 0x68, 0x73, 0x74, 0x76, 0xFF, 0xA4, 0xFF, 0xB8, 0xFF, 0xCD, 0xFF, 0xDA, 0xFF, 0xE5, 0xFF, 0xF5,
|
||||
0xFF, 0xFC, 0x41, 0x6E, 0xFC, 0x31, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x47, 0x64, 0x65, 0x67, 0x6C, 0x6D, 0x6E,
|
||||
0x73, 0xFF, 0x30, 0xFF, 0x39, 0xFF, 0x47, 0xFF, 0x5F, 0xFF, 0x74, 0xFF, 0xE0, 0xFF, 0xFD, 0x43, 0x72, 0x73, 0x6E,
|
||||
0xFC, 0x69, 0xFC, 0x69, 0xFC, 0x69, 0x21, 0x61, 0xF6, 0x41, 0x6C, 0xFC, 0x04, 0x21, 0x6C, 0xFC, 0x41, 0x61, 0xFD,
|
||||
0xE7, 0x21, 0x74, 0xFC, 0xA0, 0x09, 0x53, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0x22, 0x73, 0x69, 0xF5, 0xFB, 0xC1, 0x05,
|
||||
0x92, 0x61, 0xFB, 0x98, 0x43, 0x6E, 0x72, 0x73, 0xFB, 0x92, 0xFF, 0xFA, 0xFB, 0x92, 0x43, 0x6E, 0x72, 0x73, 0xFB,
|
||||
0x88, 0xFB, 0x88, 0xFB, 0x88, 0x42, 0xA9, 0xB3, 0xFF, 0xF6, 0xFB, 0x7E, 0x45, 0x72, 0x64, 0x65, 0xC3, 0x6D, 0xFB,
|
||||
0x77, 0xFB, 0x77, 0xFF, 0xE5, 0xFF, 0xF9, 0xFB, 0x77, 0x42, 0x6E, 0x73, 0xFB, 0x67, 0xFB, 0x67, 0x42, 0xA1, 0xAD,
|
||||
0xFB, 0x60, 0xFF, 0xC8, 0x45, 0x69, 0x61, 0x65, 0x6F, 0xC3, 0xFF, 0xE2, 0xFF, 0xF2, 0xFB, 0x59, 0xFB, 0x59, 0xFF,
|
||||
0xF9, 0x41, 0xA1, 0xFB, 0x49, 0x42, 0x61, 0xC3, 0xFB, 0x45, 0xFF, 0xFC, 0x21, 0xB1, 0xF9, 0x41, 0x62, 0xFB, 0x9C,
|
||||
0x47, 0x64, 0x65, 0x62, 0x73, 0x6E, 0xC3, 0x72, 0xFF, 0x81, 0xFF, 0x88, 0xFF, 0x9A, 0xFF, 0x8F, 0xFF, 0xDE, 0xFF,
|
||||
0xF9, 0xFF, 0xFC, 0x46, 0xC3, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xFB, 0x5A, 0xFC, 0x32, 0xFD, 0x07, 0xFE, 0x4E, 0xFF,
|
||||
0x4B, 0xFF, 0xEA, 0x41, 0x69, 0xFB, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x45, 0x63, 0x6E, 0x72, 0x73, 0x69,
|
||||
0xFA, 0xCE, 0xF4, 0xA7, 0xFB, 0x01, 0xFF, 0xE3, 0xFF, 0xFD, 0xA0, 0x11, 0x72, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x22, 0x65, 0xC3, 0xFA, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x65, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72, 0x65, 0x69,
|
||||
0xE8, 0x5B, 0xE8, 0x5E, 0xE8, 0x64, 0xE8, 0x5E, 0xE8, 0x6D, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8,
|
||||
0x5E, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x77, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x80, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E,
|
||||
0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x8A, 0xFF, 0xDC, 0xFF, 0xFD, 0x42, 0x6D, 0x72, 0xF4, 0x38, 0xF3, 0xDE, 0x41, 0x69,
|
||||
0xF3, 0xD3, 0x43, 0x6C, 0x73, 0x74, 0xF9, 0xB2, 0xFF, 0xFC, 0xF9, 0xB2, 0x42, 0x6E, 0x74, 0xF9, 0xA8, 0xF9, 0xA8,
|
||||
0x41, 0x69, 0xEB, 0x9B, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x65, 0x69, 0x6F, 0x61, 0xE7, 0xDE, 0xE7, 0xE1, 0xE7, 0xE1,
|
||||
0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7,
|
||||
0xE1, 0xE7, 0xE1, 0xF7, 0xB7, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE8, 0x0D, 0xE8, 0x0D,
|
||||
0xFF, 0xCE, 0xFF, 0xD9, 0xFF, 0xE3, 0xFF, 0xFD, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0xE7, 0x8D, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7,
|
||||
0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xF7, 0x41, 0xA0, 0x08, 0xB1, 0x21, 0x2E, 0xFD, 0x49, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x2E,
|
||||
0x73, 0xE8, 0x4E, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x57, 0xFF, 0xFA, 0xFF, 0xFD,
|
||||
0x22, 0x2E, 0x73, 0xDE, 0xE1, 0x21, 0x61, 0xFB, 0x21, 0xAD, 0xFD, 0x23, 0x6F, 0x61, 0xC3, 0xD9, 0xF5, 0xFD, 0x21,
|
||||
0x66, 0xF9, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0xE7, 0x0E, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A,
|
||||
0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7,
|
||||
0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xFF, 0xFD, 0x41, 0x73,
|
||||
0xFF, 0x84, 0x42, 0x2E, 0x65, 0xFF, 0x7D, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x42, 0x6F, 0x61, 0xFF, 0x95, 0xFF, 0xFD,
|
||||
0x21, 0x6E, 0xF9, 0x41, 0x72, 0xF9, 0x23, 0x42, 0x65, 0x72, 0xFF, 0xFC, 0xE7, 0x37, 0x21, 0x74, 0xF9, 0x42, 0x6C,
|
||||
0x73, 0xF8, 0x4D, 0xFF, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE7, 0x64, 0xE7, 0x67, 0xE7, 0x67,
|
||||
0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x6D, 0x41, 0x6E, 0xF8, 0xFB, 0x21, 0x6F, 0xFC, 0x22, 0x6F, 0x72, 0xE3,
|
||||
0xFD, 0x41, 0x63, 0xE7, 0x04, 0x21, 0x65, 0xFC, 0x41, 0x61, 0xEA, 0x8B, 0x22, 0x6E, 0x67, 0xF9, 0xFC, 0x41, 0x64,
|
||||
0xF7, 0x46, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x6F, 0x61, 0x65, 0x69, 0x75,
|
||||
0xE6, 0x5D, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6,
|
||||
0x60, 0xF6, 0x36, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60,
|
||||
0xE6, 0x60, 0xFE, 0xD0, 0xFF, 0x4F, 0xFF, 0xAC, 0xFF, 0xBD, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x2E, 0xE6,
|
||||
0x0C, 0x42, 0x2E, 0x73, 0xE6, 0x08, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x6C, 0xFB, 0x42, 0x6F, 0x61,
|
||||
0xE6, 0xD5, 0xE6, 0xD5, 0x21, 0x6E, 0xF9, 0x21, 0x61, 0xFD, 0x22, 0x65, 0x6D, 0xF0, 0xFD, 0x41, 0x65, 0xFE, 0x9F,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFA, 0x22, 0x6C, 0x69, 0xFA, 0xFD, 0x42, 0x6C,
|
||||
0x62, 0xF7, 0x7C, 0xFF, 0xFB, 0x42, 0x63, 0x6F, 0xE6, 0x55, 0xE6, 0xEB, 0x21, 0x69, 0xF9, 0x41, 0x2E, 0xE8, 0xAA,
|
||||
0x42, 0x2E, 0x73, 0xE8, 0xA6, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x47, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xE9, 0x79, 0xE9, 0xB5, 0xE9, 0xB5, 0xE9, 0xB5, 0xFF, 0xFB, 0xE9, 0xB5, 0xE9, 0xBB, 0x43,
|
||||
0x61, 0x69, 0x6F, 0xF5, 0xB9, 0xFF, 0xEA, 0xE9, 0xB0, 0x42, 0x61, 0x74, 0xF5, 0xAF, 0xE6, 0xBD, 0x41, 0x72, 0xE6,
|
||||
0x11, 0x21, 0x65, 0xFC, 0x46, 0x63, 0x6C, 0x6D, 0x70, 0x74, 0x78, 0xF1, 0xA5, 0xFF, 0xBC, 0xFF, 0xE8, 0xFF, 0xF2,
|
||||
0xFF, 0xFD, 0xFF, 0x0D, 0xA0, 0x0A, 0x13, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD,
|
||||
0xA1, 0x06, 0xF3, 0x63, 0xFD, 0x21, 0x69, 0xEC, 0x21, 0x6D, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD,
|
||||
0x21, 0x61, 0xDE, 0x21, 0x69, 0xFD, 0xA2, 0x06, 0xF3, 0x6E, 0x78, 0xF5, 0xFD, 0xA0, 0x0A, 0x43, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x07, 0x23, 0x6E, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF6, 0xA6,
|
||||
0xF6, 0xA6, 0xF6, 0xA6, 0xFF, 0xFB, 0xF6, 0xA6, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE9, 0xF3,
|
||||
0xE9, 0xCA, 0xF6, 0x93, 0xF6, 0x93, 0xFF, 0xBF, 0xFF, 0xD8, 0xF6, 0x93, 0xFF, 0xF0, 0x21, 0x72, 0xE7, 0x42, 0x65,
|
||||
0x6F, 0xFF, 0xFD, 0xE9, 0x19, 0x43, 0x64, 0x70, 0x73, 0xF0, 0xC5, 0xFF, 0xF9, 0xF1, 0x1F, 0x42, 0x6F, 0x65, 0xE9,
|
||||
0x08, 0xF0, 0xB7, 0x42, 0x6C, 0x6D, 0xF6, 0x93, 0xFF, 0xF9, 0x5B, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0x61, 0x65, 0x69, 0x6F,
|
||||
0xE4, 0xDF, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4,
|
||||
0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2,
|
||||
0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xFE, 0xF6, 0xFF, 0x10, 0xFF, 0x62, 0xFF, 0xE8, 0xFF, 0xF9, 0xD6, 0x00, 0x41,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xE4, 0x8D, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90,
|
||||
0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4,
|
||||
0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0x41, 0x6C, 0xF5, 0xF5, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72,
|
||||
0x69, 0xE4, 0x44, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47,
|
||||
0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4,
|
||||
0x47, 0xE4, 0x47, 0xE4, 0x73, 0xE4, 0x73, 0xFF, 0xFC, 0xD6, 0x00, 0x81, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xE3, 0xFC, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3,
|
||||
0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0x41, 0x75, 0xF3, 0x6B, 0x41, 0x66, 0xEF, 0x7D, 0xA0, 0x0D, 0x02, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0xA1, 0xFD, 0x44, 0x6E, 0x70, 0x74, 0xC3, 0xFF, 0xED, 0xF5, 0x4D, 0xF5, 0x4D, 0xFF, 0xFD,
|
||||
0x41, 0x61, 0xFC, 0x4E, 0x21, 0xAD, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C,
|
||||
0x65, 0x69, 0x6F, 0xE3, 0x86, 0xE3, 0x89, 0xE3, 0x8F, 0xE3, 0x89, 0xE3, 0x98, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0x89,
|
||||
0xE3, 0x89, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0xA2, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0xAB, 0xE3, 0x89, 0xE3,
|
||||
0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xFF, 0x8A, 0xFF, 0xCF, 0xFF, 0xE6, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xE3,
|
||||
0x38, 0xF4, 0x32, 0x21, 0x65, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x62, 0xFD, 0x41, 0x2E, 0xE4, 0x07, 0x21, 0x65, 0xFC,
|
||||
0x21, 0x74, 0xFD, 0x48, 0x6C, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xAB, 0xE6, 0xEC, 0xE7, 0x28, 0xE7,
|
||||
0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x2E, 0x21, 0x61, 0xE7, 0x41, 0x6E, 0xE3, 0x8F, 0x21, 0x61, 0xFC,
|
||||
0x47, 0x6F, 0x61, 0x6E, 0x67, 0x6C, 0x73, 0x74, 0xF3, 0xF5, 0xFF, 0xD0, 0xFF, 0xDA, 0xFF, 0xF6, 0xFF, 0xFD, 0xF4,
|
||||
0xA8, 0xFC, 0x8B, 0xA0, 0x05, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA0, 0x02, 0xB2, 0xCC,
|
||||
0x01, 0xA1, 0x68, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x70, 0x71, 0x73, 0x74, 0x76, 0xFF, 0xFD, 0xE4, 0xE9, 0xE4,
|
||||
0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9,
|
||||
0x41, 0x69, 0xE6, 0xCA, 0x44, 0x6E, 0x63, 0x6C, 0x78, 0xFF, 0xCF, 0xEE, 0x79, 0xFF, 0xD5, 0xFF, 0xFC, 0x41, 0x72,
|
||||
0xE8, 0xA2, 0x21, 0x61, 0xFC, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x74, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE6, 0x54, 0xFF, 0xFB, 0xE6, 0x90, 0xE6, 0x90,
|
||||
0xE6, 0x90, 0xE6, 0x90, 0xE6, 0x96, 0x21, 0x69, 0xEA, 0x41, 0x69, 0xE3, 0x9F, 0x44, 0x63, 0x6C, 0x6E, 0x72, 0xEE,
|
||||
0x37, 0xFF, 0xD2, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xE6, 0x62, 0x21, 0x6C, 0xFC, 0x43, 0x6E, 0x72, 0x74, 0xF4,
|
||||
0x02, 0xFE, 0xA2, 0xF4, 0x02, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x61, 0x69, 0x75, 0x6F, 0xE2, 0x4B, 0xE2,
|
||||
0x4E, 0xE2, 0x54, 0xE2, 0x4E, 0xE2, 0x5D, 0xE2, 0x62, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x62,
|
||||
0xF2, 0x24, 0xE2, 0x67, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x70, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2,
|
||||
0x4E, 0xE2, 0x4E, 0xFF, 0x50, 0xFF, 0xA0, 0xFF, 0xE2, 0xFF, 0xF3, 0xFF, 0xF6, 0xA0, 0x0B, 0x95, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0xC3, 0x00, 0x71, 0x7A, 0x73, 0x65, 0xE1, 0xF1, 0xE1, 0xF1, 0xFF, 0xFD, 0x41,
|
||||
0x74, 0xED, 0xA2, 0x42, 0x2E, 0x72, 0xE1, 0xDE, 0xFF, 0xFC, 0x43, 0x6D, 0x6E, 0x72, 0xF3, 0x81, 0xF3, 0x81, 0xF1,
|
||||
0x88, 0x45, 0x63, 0x66, 0x6F, 0x74, 0x75, 0xED, 0x98, 0xED, 0x98, 0xFB, 0x31, 0xF3, 0x77, 0xF2, 0xA5, 0xD9, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x6F, 0x61, 0x65, 0xE1, 0xBA, 0xE1, 0xBD, 0xE1, 0xC3, 0xE1, 0xBD, 0xE1, 0xCC, 0xE1, 0xD1,
|
||||
0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xD1, 0xE1, 0xBD, 0xE1, 0xD6, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1,
|
||||
0xBD, 0xFF, 0xCF, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xFF, 0xDF, 0xFF, 0xE6, 0xFF, 0xF0,
|
||||
0xC1, 0x0D, 0x22, 0x6F, 0xE2, 0x8F, 0x42, 0x63, 0x71, 0xFF, 0xFA, 0xF1, 0x1E, 0xC2, 0x00, 0x71, 0x2E, 0x69, 0xE1,
|
||||
0x5F, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x2E, 0x65, 0xE1, 0x56, 0xED, 0x24, 0x41, 0x74, 0xFE, 0xB9, 0x21, 0x63, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x41, 0x72, 0xE5, 0x49, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x75, 0xE1, 0x3F, 0xE1, 0x6B,
|
||||
0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1,
|
||||
0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B,
|
||||
0xE1, 0x6B, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x70, 0xE2, 0xB2, 0x42, 0x6D, 0x74, 0xFF, 0xFC, 0xEC, 0xBA, 0xD7, 0x00,
|
||||
0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x6F, 0xE0, 0xE9, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15,
|
||||
0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1,
|
||||
0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xFF, 0xF9, 0x42, 0x61, 0x6F, 0xF1, 0x95, 0xF1,
|
||||
0x95, 0x21, 0x74, 0xF9, 0x41, 0x61, 0xF4, 0x4F, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0xA0, 0x10, 0x92, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x10, 0xB2, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x23, 0x65, 0x61, 0x70, 0xE2, 0xEE, 0xFD, 0x44, 0x64, 0x72, 0x6E, 0x74, 0xF1, 0x7E, 0xFF, 0xF9,
|
||||
0xF1, 0x42, 0xF9, 0xFB, 0x41, 0x6E, 0xEB, 0x6F, 0x21, 0x6F, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x65,
|
||||
0xEC, 0x1B, 0xA0, 0x06, 0x31, 0x41, 0xB1, 0xE1, 0xF2, 0x21, 0xC3, 0xFC, 0x22, 0x2E, 0x65, 0xF6, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x73, 0xFB, 0x41, 0x61, 0xE6, 0x3D, 0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x41,
|
||||
0x6F, 0xE6, 0x2E, 0xA1, 0x04, 0xC2, 0x73, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xE4, 0x2E, 0xE4, 0x2E, 0xFF,
|
||||
0xFB, 0xE4, 0x2E, 0xE4, 0x2E, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xDF, 0xE4, 0x1B, 0xE4, 0x1B,
|
||||
0xFF, 0xD3, 0xE4, 0x1B, 0xFF, 0xE2, 0xFF, 0xF0, 0x21, 0x61, 0xEA, 0x23, 0x6E, 0x6C, 0x72, 0xA4, 0xA7, 0xFD, 0x41,
|
||||
0x7A, 0xEB, 0xBB, 0x43, 0x63, 0x65, 0x72, 0xF1, 0x9A, 0xFF, 0xFC, 0xF1, 0x9A, 0x42, 0x71, 0x63, 0xE5, 0xE7, 0xFF,
|
||||
0xAA, 0x41, 0x65, 0xFF, 0xA3, 0x42, 0x64, 0x74, 0xFD, 0x3A, 0xFF, 0xFC, 0xA2, 0x04, 0xA2, 0x72, 0x6E, 0xEE, 0xF9,
|
||||
0x41, 0x65, 0xFD, 0x36, 0x21, 0x69, 0xFC, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0xC1, 0x04, 0xA2, 0x72, 0xE1, 0x66, 0x41,
|
||||
0x71, 0xE5, 0xBC, 0xA1, 0x04, 0xC2, 0x72, 0xFC, 0x41, 0x65, 0xE5, 0xB3, 0x21, 0x74, 0xFC, 0xA1, 0x04, 0xC2, 0x73,
|
||||
0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xEF, 0xFF, 0xFB, 0xE3, 0xB0, 0xE3, 0xB0, 0xE3, 0xB0, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0x61, 0xFF, 0xC2, 0xE3, 0x9D, 0xE3, 0x9D, 0xFF, 0xD0, 0xFF, 0xD5, 0xFF,
|
||||
0xF0, 0x21, 0x69, 0xEA, 0x41, 0x6F, 0xEA, 0x8B, 0xA1, 0x04, 0x52, 0x72, 0xFC, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xE0, 0x80, 0xE0, 0x83, 0xFF, 0xFB, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x89, 0x21, 0x61, 0xEA,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x72, 0xFC, 0x7C, 0x21, 0x70, 0xFC, 0x41, 0x64, 0xFC, 0x75, 0x22, 0x6D, 0x6E, 0xF9, 0xFC,
|
||||
0xA0, 0x0E, 0x13, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x0E, 0x43, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xD8, 0x22, 0x6C, 0x73, 0xFA, 0xFD, 0x41, 0x2E, 0xE1, 0x38, 0x42, 0x2E,
|
||||
0x73, 0xE1, 0x34, 0xFF, 0xFC, 0x42, 0x61, 0x73, 0xFF, 0xF9, 0xE1, 0xD0, 0x24, 0x69, 0x6F, 0x65, 0x74, 0xC9, 0xD7,
|
||||
0xE9, 0xF9, 0x43, 0x6C, 0x72, 0x73, 0xFF, 0x8D, 0xFF, 0xB2, 0xFF, 0xF7, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x75,
|
||||
0x65, 0x61, 0x69, 0x6F, 0xDF, 0x00, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF,
|
||||
0x03, 0xDF, 0x03, 0xDF, 0x03, 0xEE, 0xD9, 0xDF, 0x03, 0xDF, 0x03, 0xFD, 0xA1, 0xFD, 0xAA, 0xDF, 0x03, 0xDF, 0x03,
|
||||
0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xFD, 0xC1, 0xFE, 0x17, 0xFE, 0x66, 0xFE, 0x95, 0xFF, 0x08, 0xFF, 0x13, 0xFF,
|
||||
0xF6, 0x42, 0x6D, 0x72, 0xDF, 0x3C, 0xEA, 0x76, 0x42, 0x65, 0x69, 0xFC, 0xC6, 0xFF, 0xF9, 0xD7, 0x00, 0x41, 0x2E,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x75, 0xDE, 0x9E, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1,
|
||||
0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE,
|
||||
0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x6E, 0x61, 0xDE, 0x5C, 0xEE,
|
||||
0xD0, 0x41, 0xA1, 0xF0, 0xDB, 0x43, 0x61, 0xC3, 0x65, 0xF0, 0xD7, 0xFF, 0xFC, 0xF0, 0xD7, 0x21, 0x69, 0xF6, 0x21,
|
||||
0x63, 0xFD, 0xA0, 0x0A, 0x72, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69,
|
||||
0xC3, 0xEE, 0xFD, 0x21, 0x6E, 0xFB, 0x42, 0x65, 0x72, 0xE2, 0x3D, 0xE9, 0xEC, 0x22, 0x69, 0x74, 0xF6, 0xF9, 0xA0,
|
||||
0x0B, 0xB1, 0x23, 0xA1, 0xA9, 0xAD, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0xC3, 0x65, 0x6F, 0xF6, 0xF9, 0xF6, 0xF6, 0x43,
|
||||
0x64, 0x6E, 0x72, 0xF5, 0xFA, 0xED, 0xB7, 0xFF, 0xF7, 0x41, 0x6D, 0xEF, 0xA6, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72,
|
||||
0x65, 0x61, 0x6F, 0xDD, 0xF5, 0xDD, 0xF8, 0xDD, 0xFE, 0xDD, 0xF8, 0xDE, 0x07, 0xDE, 0x0C, 0xDD, 0xF8, 0xDD, 0xF8,
|
||||
0xDD, 0xF8, 0xDD, 0xF8, 0xFF, 0x9F, 0xDD, 0xF8, 0xDE, 0x11, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x1A, 0xDD, 0xF8, 0xDD,
|
||||
0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x24, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xFC, 0xC4, 0x00, 0x71, 0x74,
|
||||
0x73, 0x6E, 0x61, 0xDD, 0xAD, 0xDD, 0xAD, 0xDD, 0xAD, 0xEE, 0x21, 0xA0, 0x00, 0xD1, 0x21, 0x2E, 0xFD, 0x22, 0x2E,
|
||||
0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x02, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x2E, 0x65, 0xEC, 0xFD, 0x21, 0x6C,
|
||||
0xFB, 0x22, 0x2E, 0x73, 0xEF, 0xF2, 0x21, 0x6E, 0xED, 0x21, 0xB3, 0xFD, 0x21, 0x65, 0xEA, 0x21, 0x6E, 0xFD, 0x23,
|
||||
0x61, 0xC3, 0x6F, 0xEF, 0xF7, 0xFD, 0x21, 0x6C, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xC9, 0x23, 0x2E, 0x61, 0x65,
|
||||
0xC3, 0xC9, 0xFD, 0x21, 0x72, 0xF9, 0xC6, 0x00, 0x71, 0x7A, 0x73, 0x65, 0x61, 0x69, 0x6F, 0xDD, 0x57, 0xDD, 0x57,
|
||||
0xFF, 0xBF, 0xFF, 0xD2, 0xFF, 0xF0, 0xFF, 0xFD, 0x41, 0x74, 0xDF, 0xF2, 0x21, 0x63, 0xFC, 0x41, 0x76, 0xDE, 0x67,
|
||||
0x44, 0x6E, 0x2E, 0x73, 0x6C, 0xFF, 0xF9, 0xF5, 0xEC, 0xF5, 0xEF, 0xFF, 0xFC, 0x41, 0x65, 0xFA, 0x22, 0x41, 0x76,
|
||||
0xE8, 0xEA, 0xA0, 0x0E, 0xD2, 0xA0, 0x0E, 0xF3, 0xA0, 0x0F, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21,
|
||||
0x6F, 0xF1, 0x21, 0x64, 0xFD, 0x44, 0x6C, 0x6D, 0x72, 0x75, 0xFF, 0xCF, 0xFA, 0x44, 0xFF, 0xD3, 0xFF, 0xFD, 0xA0,
|
||||
0x0F, 0x52, 0xA1, 0x0F, 0x52, 0x73, 0xFD, 0x21, 0x61, 0xFB, 0xA1, 0x04, 0x52, 0x73, 0xFD, 0x47, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xDD, 0xE5, 0xFF, 0xFB, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xEE, 0x21,
|
||||
0x65, 0xEA, 0x21, 0x72, 0xFD, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF4, 0xB1, 0xA0, 0x0F, 0xF3, 0xA1, 0x06, 0xF3, 0x72,
|
||||
0xFD, 0x41, 0x72, 0xF9, 0xC6, 0xA1, 0x06, 0xF3, 0x6F, 0xFC, 0xA0, 0x10, 0x23, 0x41, 0x2E, 0xF7, 0x64, 0x42, 0x2E,
|
||||
0x73, 0xF7, 0x60, 0xFF, 0xFC, 0x21, 0x74, 0xF9, 0x21, 0x69, 0xFD, 0xA2, 0x07, 0x23, 0x72, 0x76, 0xEC, 0xFD, 0x45,
|
||||
0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xF9, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03, 0x48, 0x72, 0x68, 0x61,
|
||||
0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE1, 0x50, 0xE1, 0x27, 0xFF, 0xC7, 0xED, 0xF0, 0xFF, 0xD0, 0xED, 0xF0, 0xED, 0xF0,
|
||||
0xFF, 0xF0, 0x21, 0x72, 0xE7, 0xC7, 0x07, 0xB1, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xDD, 0x6A, 0xDD, 0x6D,
|
||||
0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x73, 0x21, 0x61, 0xE8, 0x22, 0x65, 0x72, 0xE2, 0xFD, 0xA0,
|
||||
0x11, 0x43, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x70, 0x64, 0x72, 0xE0, 0xFD, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7A, 0x61, 0x65, 0x6F, 0x75, 0xDC, 0x19, 0xDC, 0x1C, 0xDC, 0x22, 0xDC, 0x1C, 0xDC, 0x2B, 0xDC, 0x30, 0xDC, 0x1C,
|
||||
0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x30, 0xDC, 0x1C, 0xFE, 0x72, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE,
|
||||
0xC8, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE, 0xE8, 0xFF, 0x26, 0xFF, 0x5F, 0xFF, 0xF9,
|
||||
0x41, 0x65, 0xE1, 0x23, 0x41, 0x6E, 0xDF, 0x92, 0x21, 0x69, 0xFC, 0x22, 0x74, 0x64, 0xF5, 0xFD, 0x41, 0x6C, 0xDF,
|
||||
0x86, 0x21, 0x65, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x62, 0xDF, 0x7C, 0x21, 0x6F, 0xFC, 0x41, 0x72, 0xDF, 0x75, 0x21,
|
||||
0x61, 0xFC, 0x43, 0x63, 0x70, 0x74, 0xFF, 0xF6, 0xDF, 0x6E, 0xFF, 0xFD, 0x41, 0xA1, 0xDF, 0x8F, 0x21, 0xC3, 0xFC,
|
||||
0x21, 0x6C, 0xFD, 0x24, 0x6E, 0x62, 0x6C, 0x74, 0xCF, 0xDB, 0xEC, 0xFD, 0x21, 0xA1, 0xBF, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x2E, 0xE4, 0xB3, 0x42, 0x2E, 0x73, 0xE4, 0xAF, 0xFF, 0xFC, 0x22, 0x6F, 0x61,
|
||||
0xF9, 0xF9, 0x21, 0x72, 0xFB, 0x23, 0x61, 0x6F, 0x65, 0xD8, 0xEA, 0xFD, 0x41, 0x73, 0xDE, 0x49, 0x21, 0x61, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x64, 0xE0, 0x00, 0x41, 0x6C, 0xDF, 0xFC, 0x41, 0x69, 0xE9, 0x65, 0x42,
|
||||
0x63, 0x74, 0xDF, 0xF7, 0xFF, 0xFC, 0xA4, 0x0E, 0xB2, 0x6D, 0x6E, 0x74, 0x63, 0xEA, 0xED, 0xF1, 0xF9, 0x41, 0xBA,
|
||||
0xE4, 0x87, 0x41, 0x75, 0xDF, 0xE5, 0xA2, 0x0E, 0xB2, 0xC3, 0x78, 0xF8, 0xFC, 0x41, 0x6E, 0xDF, 0xDA, 0x21, 0x61,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0xB3, 0xF0, 0x22, 0x6F, 0xC3, 0xED, 0xFD, 0x21,
|
||||
0x69, 0xFB, 0x41, 0x2E, 0xDB, 0x9E, 0x42, 0x2E, 0x73, 0xDB, 0x9A, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x41,
|
||||
0xAD, 0xDF, 0xAF, 0x43, 0x69, 0xC3, 0x65, 0xDF, 0xAB, 0xFF, 0xFC, 0xDF, 0xAB, 0x41, 0xA1, 0xDF, 0xA1, 0x43, 0x61,
|
||||
0xC3, 0x6F, 0xDF, 0x9D, 0xFF, 0xFC, 0xDF, 0x9D, 0x41, 0x61, 0xE4, 0x31, 0x21, 0x76, 0xFC, 0x41, 0x74, 0xDD, 0x80,
|
||||
0x41, 0x69, 0xDF, 0x88, 0xA1, 0x0E, 0x92, 0x72, 0xFC, 0x41, 0x76, 0xDF, 0x7F, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x69,
|
||||
0xDF, 0x7B, 0xDF, 0xF0, 0xDF, 0x7B, 0xFF, 0xF7, 0xFF, 0xFC, 0xC8, 0x0E, 0xB2, 0x62, 0x63, 0x64, 0x67, 0x6A, 0x6C,
|
||||
0x73, 0x74, 0xFF, 0x9E, 0xFF, 0xA9, 0xFF, 0xB7, 0xFF, 0xC0, 0xFF, 0xCE, 0xFF, 0xDC, 0xFF, 0xDF, 0xFF, 0xF0, 0x41,
|
||||
0x65, 0xDF, 0x49, 0x41, 0x69, 0xDD, 0x81, 0x21, 0x63, 0xFC, 0x21, 0x61, 0xFD, 0xA2, 0x0E, 0xB2, 0x63, 0x72, 0xF2,
|
||||
0xFD, 0xC3, 0x0E, 0xB2, 0x72, 0x62, 0x73, 0xDF, 0x34, 0xE1, 0xB9, 0xE0, 0xFB, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xDF, 0x28, 0xFF, 0x3B, 0xFF, 0x4E, 0xFF, 0xC4, 0xFF, 0xED, 0xFF, 0xF4, 0xE5, 0xE3, 0x21, 0x73, 0xEA,
|
||||
0x42, 0x73, 0x6E, 0xFE, 0xFB, 0xFF, 0xFD, 0x41, 0x70, 0xE6, 0x22, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x6F,
|
||||
0xDA, 0x54, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA,
|
||||
0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80,
|
||||
0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xFF, 0xF5, 0xFF, 0xFC, 0x41, 0x68, 0xEC, 0xA2, 0x21, 0x63, 0xFC, 0xC2, 0x01,
|
||||
0xF2, 0x2E, 0x73, 0xDA, 0x02, 0xFF, 0xFD, 0xC1, 0x01, 0xF2, 0x2E, 0xD9, 0xF9, 0xA0, 0x01, 0xF2, 0x42, 0x61, 0x72,
|
||||
0xF6, 0xB8, 0xDB, 0x22, 0x41, 0x65, 0xDE, 0x04, 0x42, 0x61, 0x6D, 0xDE, 0x00, 0xE5, 0xAF, 0x44, 0x74, 0x6C, 0x63,
|
||||
0x72, 0xFF, 0xEE, 0xFF, 0xF5, 0xEA, 0x58, 0xFF, 0xF9, 0x41, 0x75, 0xEC, 0x56, 0x41, 0x6F, 0xEC, 0x52, 0x22, 0x71,
|
||||
0x63, 0xF8, 0xFC, 0x21, 0x6F, 0xFB, 0x41, 0x6C, 0xEA, 0x9C, 0x41, 0x70, 0xEB, 0x6A, 0x41, 0x62, 0xE5, 0x83, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x63, 0xDA, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0xDC,
|
||||
0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x74, 0x76, 0x77, 0x79,
|
||||
0x72, 0x7A, 0x73, 0x6C, 0x78, 0x65, 0x69, 0x61, 0x6F, 0x75, 0xC3, 0xD9, 0xA2, 0xD9, 0xA5, 0xD9, 0xAB, 0xD9, 0xA5,
|
||||
0xD9, 0xB4, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xBE, 0xD9, 0xA5, 0xD9,
|
||||
0xC7, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xFF, 0x4E, 0xFF, 0xA0, 0xFF, 0xA9, 0xFF, 0xAF, 0xFF, 0xAF, 0xFF, 0xC4,
|
||||
0xFF, 0xDE, 0xFF, 0xE1, 0xFF, 0xE5, 0xFF, 0xED, 0xFF, 0xFD, 0x42, 0x63, 0x64, 0xFF, 0x62, 0xF8, 0xFA, 0xD7, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x6C, 0x72, 0x69, 0xD9, 0x44, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47,
|
||||
0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9,
|
||||
0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x73, 0xD9, 0x73, 0xFF, 0xF9, 0x41, 0x73, 0xFE, 0xF3, 0xD7, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x61, 0xD8, 0xF8, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB,
|
||||
0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8,
|
||||
0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xFF, 0xFC, 0x42, 0x6E, 0x72, 0xEA, 0x5D, 0xEA,
|
||||
0x5D, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x69, 0xD8, 0xA9, 0xD8, 0xAC, 0xD8, 0xB2, 0xD8, 0xAC, 0xD8, 0xBB,
|
||||
0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xC5, 0xD8, 0xAC, 0xD8,
|
||||
0xAC, 0xD8, 0xAC, 0xD8, 0xCE, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xFF, 0xF9, 0xF4, 0x61,
|
||||
0xD6, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xD8, 0x5E, 0xD8, 0x61, 0xD8, 0x67, 0xD8, 0x61, 0xD8, 0x70, 0xD8, 0x75, 0xD8,
|
||||
0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x75, 0xD8, 0x61, 0xD8, 0x7A, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61,
|
||||
0xD8, 0x83, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0x41, 0x6F, 0xF1, 0x80, 0xD7, 0x00, 0x41,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x6F, 0xD8, 0x15, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8,
|
||||
0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18,
|
||||
0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xFF, 0xFC, 0xC1, 0x00, 0x41, 0x2E, 0xD7, 0xCD, 0x41,
|
||||
0x73, 0xE8, 0xC1, 0xA0, 0x02, 0x82, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4,
|
||||
0x80, 0xF4, 0x80, 0xFF, 0xFB, 0x21, 0x6C, 0xF6, 0x21, 0x65, 0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4, 0x70, 0xF4, 0x70,
|
||||
0xF4, 0x70, 0x21, 0x6C, 0xF6, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0xA0, 0x02, 0xD2, 0x21, 0x2E,
|
||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x22, 0x6F, 0x61, 0xFB, 0xFB, 0x21, 0x63, 0xFB, 0x21, 0x69, 0xFD, 0x25, 0x6D,
|
||||
0x74, 0x73, 0x6E, 0x72, 0xD1, 0xD1, 0xE1, 0xE7, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xE5, 0xE5, 0xE5, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x73, 0xFD, 0x25, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xB9, 0xB9, 0xC9, 0xCF, 0xFD, 0x46, 0x73, 0x69, 0x2E, 0x64,
|
||||
0x6F, 0x72, 0xD7, 0x59, 0xFF, 0x92, 0xD7, 0x59, 0xFF, 0xDD, 0xFF, 0xC1, 0xFF, 0xF5, 0x41, 0x73, 0xFF, 0x86, 0x21,
|
||||
0x6F, 0xFC, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0x3F, 0xE8, 0x39, 0xFF, 0xFD, 0xFF, 0x78, 0xE8, 0x39, 0xA0,
|
||||
0x02, 0xA3, 0x21, 0x2E, 0xFD, 0x23, 0x2E, 0x73, 0x69, 0xFA, 0xFD, 0xE3, 0x42, 0x6F, 0x61, 0xF3, 0xEA, 0xF3, 0xEA,
|
||||
0x21, 0x63, 0xF9, 0x43, 0x65, 0x61, 0x69, 0xFF, 0xEF, 0xF3, 0xE0, 0xFF, 0xFD, 0x41, 0x6F, 0xF3, 0xD6, 0x22, 0x74,
|
||||
0x6D, 0xF2, 0xFC, 0x41, 0x73, 0xFF, 0x76, 0x21, 0x65, 0xFC, 0x21, 0x6F, 0xF9, 0x41, 0x65, 0xFF, 0x6F, 0x21, 0x6C,
|
||||
0xFC, 0x47, 0x61, 0x2E, 0x73, 0x74, 0x6D, 0x64, 0x62, 0xFF, 0xB5, 0xD6, 0xF4, 0xFF, 0xEA, 0xFF, 0xF3, 0xFF, 0xF6,
|
||||
0xFF, 0x6D, 0xFF, 0xFD, 0x21, 0x6D, 0xE0, 0x42, 0x2E, 0x65, 0xD6, 0xDB, 0xFF, 0xFD, 0x21, 0x61, 0xF6, 0xA0, 0x02,
|
||||
0xA2, 0x42, 0x2E, 0x73, 0xFF, 0xFD, 0xFF, 0xA2, 0x42, 0x2E, 0x73, 0xFF, 0x98, 0xFF, 0x9B, 0x23, 0x65, 0x6F, 0x61,
|
||||
0xF2, 0xF2, 0xF9, 0x21, 0x6C, 0xF9, 0x21, 0x65, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xEC, 0xEC, 0xEC, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0x47, 0x61, 0x65, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xFF, 0xC2,
|
||||
0xFF, 0xC2, 0xFF, 0xEA, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xFD, 0xFF, 0x08, 0x44, 0x6D, 0x74, 0x73, 0x6E, 0xFE, 0xDF,
|
||||
0xFE, 0xDF, 0xFE, 0xEF, 0xFE, 0xF5, 0x41, 0x6F, 0xFE, 0xB6, 0x43, 0x6F, 0x61, 0x65, 0xF3, 0x41, 0xF3, 0x41, 0xF3,
|
||||
0x41, 0x42, 0x2E, 0x6C, 0xD6, 0x6F, 0xFF, 0xF6, 0x21, 0x65, 0xF9, 0x41, 0x65, 0xE7, 0x5F, 0x44, 0x2E, 0x6D, 0x6C,
|
||||
0x6E, 0xD6, 0x61, 0xFF, 0xFC, 0xFF, 0xE8, 0xFF, 0xE4, 0x21, 0x65, 0xF3, 0x46, 0x6C, 0x6E, 0x6F, 0x6D, 0x74, 0x73,
|
||||
0xFE, 0xA9, 0xFF, 0xD4, 0xFE, 0x8A, 0xFF, 0xE9, 0xFF, 0xFD, 0xFF, 0xFD, 0x21, 0x6F, 0xED, 0x21, 0x64, 0xFD, 0x47,
|
||||
0x73, 0x69, 0x62, 0x72, 0x64, 0x6F, 0x6E, 0xFF, 0x5D, 0xFE, 0x71, 0xFF, 0x64, 0xFF, 0x98, 0xFF, 0xAE, 0xFE, 0xA0,
|
||||
0xFF, 0xFD, 0x41, 0x67, 0xFE, 0x9B, 0x21, 0x6F, 0xFC, 0x41, 0x63, 0xD6, 0x1E, 0x21, 0x69, 0xFC, 0x41, 0x65, 0xFF,
|
||||
0x06, 0x21, 0x74, 0xFC, 0x45, 0x2E, 0x6C, 0x74, 0x6E, 0x73, 0xD6, 0x0D, 0xFF, 0xEF, 0xFF, 0xF6, 0xE7, 0x07, 0xFF,
|
||||
0xFD, 0x45, 0xB1, 0xA9, 0xAD, 0xA1, 0xB3, 0xFE, 0x30, 0xFE, 0xA4, 0xFF, 0x09, 0xFF, 0xC5, 0xFF, 0xF0, 0xA0, 0x01,
|
||||
0x72, 0xA0, 0x01, 0x92, 0x21, 0xB3, 0xFD, 0x22, 0x75, 0xC3, 0xF7, 0xFD, 0x21, 0x65, 0xF2, 0xA0, 0x02, 0x62, 0x21,
|
||||
0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x65, 0xF5, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xE6, 0xE9, 0xFD, 0x22, 0x6F, 0x61, 0xE5, 0xF9, 0x21, 0x63, 0xFB, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0x61, 0xD4, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67,
|
||||
0xFD, 0x41, 0x67, 0xE1, 0x68, 0x23, 0xC3, 0x6F, 0x69, 0xED, 0xF9, 0xFC, 0xA0, 0x00, 0xC2, 0x21, 0x2E, 0xFD, 0x22,
|
||||
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x65, 0xF8, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73,
|
||||
0x6D, 0xE9, 0xEC, 0xFD, 0x44, 0x2E, 0x6F, 0x61, 0x74, 0xD5, 0x78, 0xFF, 0xE8, 0xFF, 0xF9, 0xF5, 0x24, 0x42, 0x6F,
|
||||
0x61, 0xD9, 0x83, 0xD9, 0x83, 0x21, 0x74, 0xF9, 0x41, 0x6E, 0xF2, 0xAF, 0x43, 0x63, 0x74, 0x65, 0xE7, 0x07, 0xE7,
|
||||
0x07, 0xFD, 0x93, 0x41, 0x74, 0xE6, 0xFD, 0x41, 0x69, 0xE5, 0x70, 0x41, 0x61, 0xD6, 0xF0, 0x21, 0xAD, 0xFC, 0x21,
|
||||
0xC3, 0xFD, 0xA1, 0x04, 0xA2, 0x70, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xD9, 0x07, 0xD9, 0x43,
|
||||
0xFF, 0xFB, 0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x49, 0x21, 0x6F, 0xEA, 0x22, 0x6E, 0x74, 0xD4, 0xFD, 0xA0,
|
||||
0x00, 0x91, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x0F, 0x72, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD,
|
||||
0x22, 0x6F, 0x61, 0xFB, 0xFB, 0xA0, 0x03, 0x32, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x10, 0xF3,
|
||||
0x21, 0x2E, 0xFD, 0x23, 0x61, 0x2E, 0x73, 0xF5, 0xFA, 0xFD, 0x21, 0x73, 0xEB, 0x22, 0x2E, 0x65, 0xE5, 0xFD, 0x21,
|
||||
0x6C, 0xFB, 0x22, 0x65, 0x61, 0xEE, 0xFD, 0x22, 0x63, 0x64, 0xD3, 0xFB, 0x4D, 0x65, 0x61, 0x2E, 0x78, 0x6C, 0x73,
|
||||
0x63, 0x6D, 0x6E, 0x70, 0x72, 0x6F, 0x69, 0xFE, 0xF1, 0xFE, 0xF6, 0xD4, 0xD5, 0xFF, 0x04, 0xFF, 0x3B, 0xFF, 0x60,
|
||||
0xFF, 0x74, 0xFF, 0x77, 0xFF, 0x7B, 0xFF, 0x85, 0xFF, 0xB5, 0xFF, 0xC0, 0xFF, 0xFB, 0x42, 0x65, 0xC3, 0xFE, 0xC0,
|
||||
0xFE, 0xC6, 0xA0, 0x03, 0x23, 0x21, 0x2E, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x43, 0x2E,
|
||||
0x73, 0x69, 0xD4, 0x97, 0xE5, 0x91, 0xFC, 0xD0, 0x21, 0x65, 0xF6, 0x41, 0x73, 0xFE, 0xB1, 0x44, 0x2E, 0x73, 0x69,
|
||||
0x6E, 0xFE, 0xAA, 0xFE, 0xAD, 0xFF, 0xFC, 0xFE, 0xAD, 0x43, 0x2E, 0x74, 0x65, 0xD4, 0x79, 0xFF, 0xEC, 0xFF, 0xF3,
|
||||
0xA0, 0x0C, 0xF1, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x11, 0x22, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0x6F, 0x69, 0x65, 0xC7, 0xEB, 0xFD, 0x42,
|
||||
0x6F, 0x72, 0xD4, 0x4A, 0xE0, 0x14, 0x45, 0x2E, 0x64, 0x66, 0x67, 0x74, 0xD4, 0x43, 0xFF, 0xF9, 0xF1, 0x94, 0xE5,
|
||||
0xEC, 0xFA, 0x5A, 0x41, 0x65, 0xFC, 0x6C, 0x42, 0x6E, 0x73, 0xFE, 0x56, 0xFE, 0x56, 0x41, 0x6F, 0xFF, 0x9E, 0x41,
|
||||
0x73, 0xFE, 0x48, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFE, 0x44, 0xFE, 0x47, 0xFF, 0xF8, 0xFF, 0xFC, 0xFE, 0x47,
|
||||
0x42, 0x61, 0x73, 0xFF, 0xF0, 0xFE, 0x37, 0x43, 0x2E, 0x73, 0x69, 0xFE, 0x2D, 0xFE, 0x30, 0xFF, 0x7F, 0x43, 0x73,
|
||||
0x2E, 0x6E, 0xFE, 0x26, 0xFE, 0x23, 0xFE, 0x26, 0x23, 0xAD, 0xA9, 0xA1, 0xE5, 0xEC, 0xF6, 0x45, 0x6D, 0x2E, 0x73,
|
||||
0x69, 0x6E, 0xFF, 0xC6, 0xFE, 0x12, 0xFE, 0x15, 0xFF, 0x64, 0xFE, 0x15, 0xA0, 0x03, 0x22, 0x21, 0x2E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x41, 0x65, 0xFF, 0x32, 0x42, 0x2E, 0x73, 0xFF, 0x2B, 0xFF, 0x2E, 0x23, 0x65, 0x61, 0x6F, 0xF9, 0xF9,
|
||||
0xF9, 0x41, 0x73, 0xFF, 0x20, 0x21, 0x6F, 0xFC, 0x41, 0x68, 0xD7, 0xC2, 0xC2, 0x00, 0xD1, 0x2E, 0x73, 0xFD, 0xDC,
|
||||
0xFD, 0xDF, 0x22, 0x6F, 0x61, 0xF7, 0xF7, 0x4C, 0x6F, 0xC3, 0x65, 0x61, 0x2E, 0x6D, 0x74, 0x6C, 0x73, 0x6E, 0x63,
|
||||
0x69, 0xFF, 0x7B, 0xFF, 0xB5, 0xFF, 0xBC, 0xFF, 0x24, 0xD3, 0xAA, 0xFF, 0xD2, 0xFF, 0xD5, 0xFF, 0xE0, 0xFF, 0xD5,
|
||||
0xFF, 0xEB, 0xFF, 0xEE, 0xFF, 0xFB, 0x41, 0x61, 0xFE, 0xFF, 0x43, 0x65, 0x6F, 0x61, 0xF0, 0x49, 0xF0, 0x49, 0xFB,
|
||||
0xBA, 0x43, 0x2E, 0x61, 0x65, 0xFD, 0x9B, 0xFD, 0xA1, 0xFE, 0xED, 0x43, 0x2E, 0x73, 0x72, 0xFD, 0x91, 0xFD, 0x94,
|
||||
0xFF, 0xF6, 0x48, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x6F, 0x61, 0x65, 0xD3, 0x63, 0xFC, 0xFE, 0xFC, 0xFE, 0xFF, 0xE2,
|
||||
0xFC, 0xE6, 0xFF, 0xF6, 0xFD, 0x8D, 0xE3, 0xDD, 0xA0, 0x05, 0x41, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
||||
0xFD, 0x41, 0x6E, 0xFD, 0x65, 0x21, 0xB3, 0xFC, 0x41, 0x65, 0xFE, 0xAD, 0x21, 0x6E, 0xFC, 0x22, 0xC3, 0x6F, 0xF6,
|
||||
0xFD, 0x43, 0x74, 0x61, 0x69, 0xE4, 0xD8, 0xFF, 0xEA, 0xFF, 0xFB, 0x41, 0x72, 0xE4, 0xCE, 0x41, 0x64, 0xFE, 0xBA,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x72, 0x69, 0xE4,
|
||||
0xB7, 0xFF, 0xFD, 0x41, 0x69, 0xE2, 0xB7, 0x41, 0x74, 0xED, 0x7B, 0x43, 0x64, 0x73, 0x74, 0xEA, 0xF2, 0xFF, 0xFC,
|
||||
0xE4, 0xA8, 0x41, 0x73, 0xEC, 0xE8, 0x42, 0x2E, 0x65, 0xD2, 0xF0, 0xFF, 0xFC, 0xA0, 0x08, 0xF1, 0x21, 0x2E, 0xFD,
|
||||
0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x73, 0xFD, 0x21, 0xAD, 0xFD, 0x53, 0x61, 0x69, 0x73, 0x2E,
|
||||
0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0x6F, 0x63, 0x65, 0x66, 0x67, 0x70, 0x75, 0x6C, 0xC3, 0xFE, 0x25, 0xFE, 0x38,
|
||||
0xFE, 0x59, 0xD2, 0xD2, 0xFE, 0x81, 0xFE, 0x8F, 0xFE, 0x9F, 0xFF, 0x28, 0xFF, 0x4D, 0xFF, 0x6F, 0xFB, 0x0B, 0xFF,
|
||||
0xA7, 0xFF, 0xB1, 0xFF, 0xC8, 0xFF, 0xB1, 0xFF, 0xCF, 0xFF, 0xD7, 0xFF, 0xE5, 0xFF, 0xFD, 0xA0, 0x01, 0xB2, 0x21,
|
||||
0xA1, 0xFD, 0x43, 0xC3, 0x65, 0x73, 0xFF, 0xFD, 0xD2, 0xF0, 0xE3, 0x8C, 0x41, 0x65, 0xEB, 0xDA, 0x21, 0x6C, 0xFC,
|
||||
0x41, 0x65, 0xE4, 0xF6, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x43, 0x2E, 0x63, 0x74, 0xD2, 0x77, 0xFF, 0xF3, 0xFF,
|
||||
0xFD, 0x41, 0x61, 0xD2, 0x6D, 0x21, 0x63, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x6F, 0xD5, 0x4C, 0x43, 0x6F, 0x62, 0x69,
|
||||
0xFD, 0xD5, 0xFF, 0xF9, 0xFF, 0xFC, 0xA0, 0x01, 0xB1, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x65, 0xFD, 0x43,
|
||||
0x65, 0x6F, 0x72, 0xEC, 0xC5, 0xD6, 0x64, 0xDE, 0x0C, 0x45, 0x2E, 0x68, 0x64, 0x65, 0x74, 0xD2, 0x3F, 0xFF, 0xF3,
|
||||
0xE3, 0xEC, 0xEB, 0xCF, 0xFF, 0xF6, 0xA0, 0x04, 0x13, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x42, 0x2E, 0x65, 0xFC, 0x44, 0xFF, 0xFD, 0xA0, 0x03, 0xC2, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x61, 0xED, 0x22, 0x61, 0x65, 0xEA, 0xEA, 0x46, 0x73, 0x2E, 0x6E, 0x69, 0x62, 0x72, 0xFF, 0xE8, 0xFC, 0x2C, 0xFC,
|
||||
0x2F, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFB, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFC, 0x19, 0xFC, 0x1C, 0xFD, 0xCD,
|
||||
0xFD, 0x6B, 0xFC, 0x1C, 0x42, 0x73, 0x61, 0xFC, 0x0C, 0xFF, 0xF0, 0x43, 0xA9, 0xA1, 0xAD, 0xFD, 0xD5, 0xFF, 0xD6,
|
||||
0xFF, 0xF9, 0xA0, 0x02, 0xF3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6D, 0xFD, 0x43, 0x65,
|
||||
0x61, 0x6F, 0xEE, 0x8D, 0xEE, 0x8D, 0xEE, 0x8D, 0x43, 0x2E, 0x73, 0x69, 0xFF, 0xA2, 0xFF, 0xA5, 0xFF, 0xA8, 0x21,
|
||||
0x65, 0xF6, 0xA0, 0x03, 0xE3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x24, 0x2E, 0x73, 0x69, 0x6E, 0xF7, 0xFA, 0xFD,
|
||||
0xFA, 0x43, 0x2E, 0x74, 0x65, 0xFF, 0x83, 0xFF, 0xEB, 0xFF, 0xF7, 0x21, 0x6F, 0xEA, 0x41, 0x65, 0xFF, 0x7C, 0x21,
|
||||
0x6E, 0xE0, 0x21, 0x73, 0xDA, 0x25, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0xDA, 0xF3, 0xFD, 0xDA, 0x22, 0x61, 0x73,
|
||||
0xF5, 0xCF, 0x23, 0x2E, 0x73, 0x69, 0xC7, 0xCA, 0xCD, 0x23, 0x73, 0x2E, 0x6E, 0xC3, 0xC0, 0xC3, 0x23, 0xAD, 0xA9,
|
||||
0xA1, 0xED, 0xF2, 0xF9, 0x45, 0x6D, 0x2E, 0x73, 0x69, 0x6E, 0xFF, 0xCE, 0xFF, 0xB2, 0xFF, 0xB5, 0xFF, 0xB8, 0xFF,
|
||||
0xB5, 0x44, 0x6F, 0xC3, 0x65, 0x61, 0xFF, 0xC5, 0xFF, 0xE9, 0xFF, 0xF0, 0xFF, 0xAB, 0xA0, 0x10, 0x54, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xEE, 0xF1,
|
||||
0xFD, 0x21, 0x65, 0xF9, 0x42, 0x61, 0x6C, 0xFF, 0x82, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFF, 0x72, 0xFF, 0x75, 0x43,
|
||||
0x2E, 0x61, 0x65, 0xFF, 0x6B, 0xFF, 0xF9, 0xFF, 0x71, 0x43, 0x2E, 0x73, 0x72, 0xFF, 0x61, 0xFF, 0x64, 0xFF, 0xF6,
|
||||
0x43, 0x2E, 0x6F, 0x61, 0xFE, 0xEC, 0xFF, 0xF6, 0xFF, 0xE5, 0x47, 0x73, 0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0xFF,
|
||||
0x5F, 0xFF, 0x69, 0xFE, 0xE5, 0xFF, 0x6C, 0xFF, 0xAB, 0xFF, 0xD4, 0xFF, 0xF6, 0x42, 0x2E, 0x65, 0xFB, 0x09, 0xFC,
|
||||
0x5B, 0x21, 0x64, 0xF9, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x45, 0x2E, 0x65, 0x61, 0x6D, 0x69, 0xFA, 0xF9, 0xFC,
|
||||
0x4B, 0xFA, 0xFF, 0xFB, 0x10, 0xFF, 0xFD, 0x21, 0x72, 0xF0, 0x21, 0x6F, 0xFD, 0x4B, 0xC3, 0x65, 0x2E, 0x6D, 0x74,
|
||||
0x6C, 0x73, 0x6E, 0x6F, 0x61, 0x69, 0xFE, 0xE1, 0xFE, 0xF7, 0xD0, 0xBF, 0xFA, 0x5A, 0xFA, 0x5A, 0xFE, 0xFA, 0xFA,
|
||||
0x5A, 0xFA, 0x42, 0xFC, 0x35, 0xFF, 0xC4, 0xFF, 0xFD, 0x46, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x72, 0xD0, 0x9D, 0xFA,
|
||||
0x38, 0xFA, 0x38, 0xFD, 0x1C, 0xFA, 0x20, 0xFA, 0xCC, 0x41, 0x61, 0xE1, 0x84, 0x21, 0x6C, 0xFC, 0x21, 0x64, 0xFD,
|
||||
0x41, 0x6F, 0xFB, 0x7E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x2E, 0xE3,
|
||||
0x46, 0x42, 0x2E, 0x73, 0xE3, 0x42, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x69, 0xFB, 0x23, 0x64, 0x6D,
|
||||
0x63, 0xD7, 0xEA, 0xFD, 0xA0, 0x00, 0x81, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA1, 0xFD,
|
||||
0x42, 0x6F, 0x72, 0xD4, 0x62, 0xDC, 0x11, 0xA0, 0x11, 0x92, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x44, 0x61, 0x6F, 0x74, 0x75, 0xE0, 0xA2,
|
||||
0xE9, 0x8F, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x6E, 0xE1, 0xC8, 0x42, 0x63, 0x72, 0xE1, 0xC4, 0xE1, 0xC4, 0x42, 0x6D,
|
||||
0x72, 0xD9, 0x69, 0xD4, 0xC3, 0x41, 0x67, 0xD9, 0xBC, 0x42, 0x61, 0x6F, 0xD0, 0x9B, 0xD0, 0x9B, 0x43, 0x61, 0x65,
|
||||
0x6F, 0xD0, 0x94, 0xD0, 0x94, 0xD0, 0x94, 0x21, 0x69, 0xF6, 0x44, 0x61, 0x69, 0x65, 0x6F, 0xD0, 0x87, 0xD0, 0x87,
|
||||
0xD0, 0x87, 0xD0, 0x87, 0x44, 0x6A, 0x67, 0x6C, 0x6D, 0xFF, 0xDF, 0xD9, 0x97, 0xFF, 0xF0, 0xFF, 0xF3, 0x44, 0xA1,
|
||||
0xA9, 0xB3, 0xAD, 0xFF, 0xC7, 0xFF, 0xCE, 0xD8, 0x5C, 0xFF, 0xF3, 0x41, 0x72, 0xDC, 0x2A, 0x21, 0x65, 0xFC, 0x41,
|
||||
0x6D, 0xE2, 0x99, 0x21, 0x75, 0xFC, 0x41, 0x69, 0xD1, 0xE0, 0x44, 0x63, 0x67, 0x6C, 0x6D, 0xFF, 0xF2, 0xD9, 0x83,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xD2, 0x2C, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x41, 0x69, 0xD7, 0xE1, 0x21,
|
||||
0x75, 0xFC, 0x43, 0x63, 0x67, 0x71, 0xD1, 0xB0, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x61, 0xC3, 0x6F, 0xD1, 0xA3, 0xD9,
|
||||
0x2F, 0xD1, 0xA3, 0x41, 0xAD, 0xD1, 0x99, 0x43, 0x65, 0x69, 0xC3, 0xD1, 0x95, 0xD1, 0x95, 0xFF, 0xFC, 0x42, 0x69,
|
||||
0x61, 0xD7, 0x0B, 0xD1, 0x8E, 0x44, 0xA1, 0xAD, 0xA9, 0xB3, 0xD1, 0x84, 0xD1, 0x84, 0xD1, 0x84, 0xD1, 0x84, 0x45,
|
||||
0x61, 0xC3, 0x69, 0x65, 0x6F, 0xD1, 0x77, 0xFF, 0xF3, 0xD1, 0x77, 0xD1, 0x77, 0xD1, 0x77, 0x41, 0x6F, 0xD1, 0xE6,
|
||||
0x25, 0x6A, 0x67, 0x6C, 0x6D, 0x74, 0xC0, 0xCE, 0xD8, 0xEC, 0xFC, 0x41, 0xB3, 0xD1, 0x58, 0x21, 0xC3, 0xFC, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x72, 0xFF, 0x7F, 0x41, 0xA9, 0xD1, 0x4D, 0x43, 0x63, 0x71, 0x73, 0xD1, 0x46, 0xD1, 0x46, 0xD6,
|
||||
0x27, 0x22, 0xC3, 0x69, 0xF2, 0xF6, 0x41, 0x6D, 0xD1, 0xA5, 0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF9, 0xFD, 0x41,
|
||||
0x71, 0xD1, 0x2B, 0x21, 0x73, 0xFC, 0x41, 0x61, 0xD1, 0x35, 0x21, 0x6C, 0xFC, 0x47, 0x62, 0x6E, 0x63, 0x74, 0x67,
|
||||
0x65, 0x70, 0xFF, 0xCC, 0xD8, 0xD5, 0xFF, 0xCF, 0xFF, 0xE1, 0xFF, 0xED, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x72, 0x74,
|
||||
0x63, 0xD1, 0x07, 0xD1, 0x07, 0xD1, 0x07, 0x21, 0x61, 0xF6, 0x42, 0x62, 0x64, 0xD8, 0xB2, 0xFF, 0xFD, 0x41, 0x72,
|
||||
0xD0, 0x12, 0xA0, 0x0D, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x48, 0xC3, 0x61, 0x65, 0x69,
|
||||
0x6F, 0x75, 0x74, 0x70, 0xFE, 0xF9, 0xFF, 0x18, 0xFF, 0x36, 0xFF, 0x80, 0xFF, 0xC6, 0xFF, 0xE9, 0xFF, 0xF0, 0xFF,
|
||||
0xFD, 0x41, 0x72, 0xFA, 0x54, 0x21, 0x74, 0xFC, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x65, 0xC3, 0xFA, 0xFD,
|
||||
0x4F, 0x6F, 0x73, 0x2E, 0x6D, 0x6E, 0x72, 0x64, 0x65, 0x61, 0xC3, 0x63, 0x74, 0x75, 0x78, 0x6C, 0xFC, 0x13, 0xFC,
|
||||
0x2E, 0xCE, 0xA5, 0xFC, 0x46, 0xFC, 0x66, 0xFD, 0xE6, 0xFE, 0x08, 0xFE, 0x22, 0xFE, 0x48, 0xFE, 0x5B, 0xFE, 0x7D,
|
||||
0xFE, 0x8A, 0xFE, 0x8E, 0xFF, 0xD5, 0xFF, 0xFB, 0x43, 0x2E, 0x73, 0x6D, 0xF8, 0x9B, 0xF8, 0x9E, 0xFA, 0x4F, 0xA0,
|
||||
0x03, 0x53, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x84, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xF0, 0xF0, 0xFB, 0x22, 0x2E, 0x6C, 0xE3, 0xF9, 0x21, 0x65, 0xFB, 0x23, 0x65,
|
||||
0x61, 0x6F, 0xE1, 0xE1, 0xE1, 0x23, 0x65, 0x6F, 0x61, 0xDA, 0xDA, 0xDA, 0x21, 0x6C, 0xF9, 0x24, 0x6D, 0x74, 0x6C,
|
||||
0x65, 0xEC, 0xEC, 0xEF, 0xFD, 0x22, 0x2E, 0x6C, 0xC1, 0xED, 0x21, 0x73, 0xFB, 0x21, 0x6F, 0xFD, 0x23, 0x73, 0x6E,
|
||||
0x6F, 0xEC, 0xFD, 0xFA, 0x21, 0x6F, 0xF9, 0x43, 0x73, 0x69, 0x6D, 0xF8, 0x40, 0xF9, 0x8F, 0xFF, 0xFD, 0x21, 0xA1,
|
||||
0xF6, 0x43, 0x6F, 0x61, 0xC3, 0xF8, 0x33, 0xFF, 0x95, 0xFF, 0xFD, 0x41, 0x69, 0xF9, 0x78, 0x21, 0x74, 0xFC, 0x41,
|
||||
0x73, 0xF8, 0xEC, 0x42, 0x2E, 0x65, 0xF8, 0xE5, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x43, 0x69, 0x61, 0x65, 0xFF, 0xEF,
|
||||
0xFF, 0xFD, 0xF8, 0x1C, 0x41, 0x61, 0xEA, 0xAB, 0x42, 0x6F, 0x69, 0xCE, 0x5D, 0xCE, 0xBE, 0x21, 0x6D, 0xF9, 0x41,
|
||||
0x69, 0xCE, 0xB4, 0x21, 0x6D, 0xFC, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x44, 0x6D, 0x74, 0x6C, 0x6F,
|
||||
0xF6, 0xB8, 0xFF, 0xE3, 0xFF, 0xFB, 0xE7, 0x2D, 0xC3, 0x02, 0x91, 0x61, 0xC3, 0x65, 0xCF, 0xCC, 0xD7, 0x58, 0xCF,
|
||||
0xCC, 0x21, 0x69, 0xF4, 0x21, 0x63, 0xFD, 0xC1, 0x05, 0x81, 0x61, 0xCE, 0x3D, 0x21, 0x69, 0xFA, 0x21, 0x63, 0xFD,
|
||||
0x21, 0xAD, 0xFD, 0xA0, 0x02, 0xB1, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x23, 0x62, 0x65, 0xC3,
|
||||
0xF4, 0xFA, 0xFD, 0x21, 0x62, 0xED, 0x21, 0x6C, 0xEA, 0x21, 0x6D, 0xE7, 0x21, 0x69, 0xE7, 0x21, 0x70, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0x25, 0xAD, 0xA1, 0xA9, 0xBA, 0xB3, 0xEE, 0xF1, 0xE1, 0xF4, 0xFD, 0x22, 0x74, 0x69, 0xD0, 0xD0, 0x21,
|
||||
0x6E, 0xCE, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x72, 0xF5, 0xFD, 0x25, 0x69, 0xC3, 0x61, 0x65, 0x75, 0xCC, 0xE5, 0xD6,
|
||||
0xFB, 0xD9, 0x41, 0x75, 0xEA, 0x4B, 0xA0, 0x0B, 0xE3, 0x22, 0x75, 0x74, 0xFD, 0xFD, 0x22, 0x73, 0x64, 0xFB, 0xF8,
|
||||
0xA0, 0x0C, 0x63, 0x21, 0x72, 0xFD, 0xA0, 0x0C, 0x93, 0x21, 0x2E, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xEF, 0xF7, 0xFD,
|
||||
0x41, 0x73, 0xEA, 0x44, 0x21, 0xA9, 0xFC, 0xA0, 0x0A, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0xA0, 0x0C, 0x42, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x65, 0xFD, 0x24, 0x69, 0xC3, 0x65,
|
||||
0x72, 0xD7, 0xE2, 0xEE, 0xFD, 0x21, 0x72, 0xF7, 0x42, 0x65, 0x72, 0xFF, 0xFD, 0xCE, 0x2D, 0x41, 0x72, 0xFC, 0xB4,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x72, 0xCD, 0xC6, 0x21, 0x65, 0xFC, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0xC3, 0x68, 0x66, 0x6D, 0x74, 0x6F, 0x61, 0x64, 0x67, 0xFF, 0x2D,
|
||||
0xFF, 0x3C, 0xFF, 0x7F, 0xFD, 0xF7, 0xFF, 0x8A, 0xFF, 0xDC, 0xE9, 0x9F, 0xE9, 0x9F, 0xFF, 0xED, 0xFF, 0xFD, 0x41,
|
||||
0x65, 0xD8, 0x86, 0x43, 0x6E, 0x2E, 0x73, 0xD8, 0x7E, 0xF7, 0x21, 0xF7, 0x24, 0x42, 0x6F, 0x61, 0xFF, 0xF6, 0xF7,
|
||||
0x1D, 0x42, 0x2E, 0x73, 0xF8, 0xC5, 0xF8, 0xC8, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x41, 0x2E, 0xEE, 0x81, 0x21, 0x73,
|
||||
0xFC, 0x21, 0x65, 0xFD, 0x44, 0x6E, 0x72, 0x2E, 0x73, 0xFF, 0xF1, 0xFF, 0xFD, 0xF7, 0x72, 0xF7, 0x75, 0x41, 0x61,
|
||||
0xDE, 0x29, 0x41, 0x72, 0xF8, 0xA1, 0x42, 0x2E, 0x73, 0xF7, 0x5D, 0xF7, 0x60, 0x4A, 0x67, 0x64, 0x73, 0x6E, 0x62,
|
||||
0x63, 0x61, 0x74, 0x65, 0x6F, 0xFE, 0x65, 0xFE, 0x84, 0xFE, 0xAB, 0xFF, 0x9A, 0xFF, 0xB9, 0xFF, 0xC7, 0xFF, 0xE4,
|
||||
0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xF9, 0x41, 0x69, 0xFB, 0xFC, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x74, 0xFD,
|
||||
0x68, 0xA0, 0x11, 0xB2, 0x42, 0x64, 0x74, 0xFF, 0xFD, 0xFC, 0x01, 0x21, 0x69, 0xF9, 0x21, 0x73, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x74, 0x6C, 0x6E, 0xDD, 0xE0, 0xFD, 0x5C, 0x62,
|
||||
0x2E, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0xC3, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xCD, 0x5C, 0xDB, 0x8C, 0xDD, 0xF1, 0xE3, 0xC6, 0xE4, 0x43, 0xE5,
|
||||
0xC4, 0xE7, 0x42, 0xE7, 0x94, 0xE7, 0xDD, 0xE8, 0x9B, 0xE9, 0xD6, 0xEA, 0x67, 0xED, 0x21, 0xED, 0x83, 0xEE, 0x2C,
|
||||
0xF0, 0x08, 0xF2, 0x7F, 0xF2, 0xDD, 0xF3, 0x29, 0xF3, 0x78, 0xF3, 0xC3, 0xF4, 0x0C, 0xF6, 0x24, 0xF7, 0x4C, 0xF9,
|
||||
0x4F, 0xFD, 0x7C, 0xFF, 0xB0, 0xFF, 0xF9,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns es_patterns = {
|
||||
es_trie_data,
|
||||
sizeof(es_trie_data),
|
||||
};
|
||||
@ -1,383 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t fr_trie_data[] = {
|
||||
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C,
|
||||
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F,
|
||||
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C,
|
||||
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D,
|
||||
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E,
|
||||
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16,
|
||||
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16,
|
||||
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C,
|
||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD,
|
||||
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF,
|
||||
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72,
|
||||
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD,
|
||||
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
||||
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61,
|
||||
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74,
|
||||
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75,
|
||||
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF,
|
||||
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD,
|
||||
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41,
|
||||
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62,
|
||||
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06,
|
||||
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21,
|
||||
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
||||
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF,
|
||||
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65,
|
||||
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02,
|
||||
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5,
|
||||
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8,
|
||||
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64,
|
||||
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67,
|
||||
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01,
|
||||
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1,
|
||||
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE,
|
||||
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75,
|
||||
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC,
|
||||
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04,
|
||||
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41,
|
||||
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04,
|
||||
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61,
|
||||
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0,
|
||||
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04,
|
||||
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3,
|
||||
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73,
|
||||
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
||||
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF,
|
||||
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41,
|
||||
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72,
|
||||
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05,
|
||||
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E,
|
||||
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44,
|
||||
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86,
|
||||
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D,
|
||||
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41,
|
||||
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A,
|
||||
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F,
|
||||
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75,
|
||||
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61,
|
||||
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21,
|
||||
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
||||
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21,
|
||||
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6,
|
||||
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F,
|
||||
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68,
|
||||
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF,
|
||||
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74,
|
||||
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01,
|
||||
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA,
|
||||
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73,
|
||||
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E,
|
||||
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21,
|
||||
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21,
|
||||
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75,
|
||||
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61,
|
||||
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA,
|
||||
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
||||
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21,
|
||||
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF,
|
||||
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43,
|
||||
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21,
|
||||
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB,
|
||||
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE,
|
||||
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF,
|
||||
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41,
|
||||
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25,
|
||||
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC,
|
||||
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70,
|
||||
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC,
|
||||
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
||||
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41,
|
||||
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08,
|
||||
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9,
|
||||
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF,
|
||||
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75,
|
||||
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD,
|
||||
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18,
|
||||
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2,
|
||||
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65,
|
||||
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF,
|
||||
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63,
|
||||
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69,
|
||||
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21,
|
||||
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21,
|
||||
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7,
|
||||
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
||||
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2,
|
||||
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47,
|
||||
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7,
|
||||
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9,
|
||||
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63,
|
||||
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8,
|
||||
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75,
|
||||
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6,
|
||||
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6,
|
||||
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61,
|
||||
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6,
|
||||
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8,
|
||||
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61,
|
||||
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8,
|
||||
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
||||
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0,
|
||||
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
|
||||
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21,
|
||||
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63,
|
||||
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1,
|
||||
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70,
|
||||
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21,
|
||||
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21,
|
||||
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03,
|
||||
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76,
|
||||
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2,
|
||||
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
||||
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21,
|
||||
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4,
|
||||
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2,
|
||||
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE,
|
||||
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65,
|
||||
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF,
|
||||
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF,
|
||||
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
||||
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92,
|
||||
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF,
|
||||
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F,
|
||||
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD,
|
||||
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41,
|
||||
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD,
|
||||
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA,
|
||||
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21,
|
||||
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2,
|
||||
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59,
|
||||
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE,
|
||||
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00,
|
||||
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
||||
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0,
|
||||
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75,
|
||||
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7,
|
||||
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69,
|
||||
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C,
|
||||
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2,
|
||||
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC,
|
||||
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD,
|
||||
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3,
|
||||
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41,
|
||||
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74,
|
||||
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
||||
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9,
|
||||
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21,
|
||||
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41,
|
||||
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73,
|
||||
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34,
|
||||
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E,
|
||||
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A,
|
||||
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21,
|
||||
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9,
|
||||
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA,
|
||||
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42,
|
||||
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB,
|
||||
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1,
|
||||
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41,
|
||||
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
||||
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63,
|
||||
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD,
|
||||
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3,
|
||||
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65,
|
||||
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62,
|
||||
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD,
|
||||
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01,
|
||||
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9,
|
||||
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1,
|
||||
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21,
|
||||
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75,
|
||||
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
||||
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0,
|
||||
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47,
|
||||
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00,
|
||||
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0,
|
||||
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69,
|
||||
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA,
|
||||
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22,
|
||||
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22,
|
||||
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73,
|
||||
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF,
|
||||
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1,
|
||||
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F,
|
||||
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C,
|
||||
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66,
|
||||
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
||||
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63,
|
||||
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD,
|
||||
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F,
|
||||
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21,
|
||||
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72,
|
||||
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61,
|
||||
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF,
|
||||
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68,
|
||||
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61,
|
||||
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61,
|
||||
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB,
|
||||
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
||||
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1,
|
||||
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
||||
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C,
|
||||
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F,
|
||||
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F,
|
||||
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF,
|
||||
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6,
|
||||
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45,
|
||||
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21,
|
||||
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79,
|
||||
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D,
|
||||
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D,
|
||||
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
||||
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8,
|
||||
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7,
|
||||
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F,
|
||||
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
||||
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72,
|
||||
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB,
|
||||
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0,
|
||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5,
|
||||
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA,
|
||||
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D,
|
||||
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1,
|
||||
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D,
|
||||
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5,
|
||||
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2,
|
||||
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4,
|
||||
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87,
|
||||
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2,
|
||||
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C,
|
||||
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
||||
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69,
|
||||
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
||||
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF,
|
||||
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02,
|
||||
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72,
|
||||
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75,
|
||||
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65,
|
||||
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76,
|
||||
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9,
|
||||
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66,
|
||||
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E,
|
||||
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64,
|
||||
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21,
|
||||
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
||||
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21,
|
||||
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65,
|
||||
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF,
|
||||
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11,
|
||||
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21,
|
||||
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6,
|
||||
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF,
|
||||
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82,
|
||||
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F,
|
||||
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74,
|
||||
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74,
|
||||
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73,
|
||||
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3,
|
||||
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D,
|
||||
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1,
|
||||
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2,
|
||||
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65,
|
||||
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD,
|
||||
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1,
|
||||
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76,
|
||||
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21,
|
||||
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42,
|
||||
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2,
|
||||
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74,
|
||||
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42,
|
||||
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41,
|
||||
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2,
|
||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0,
|
||||
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
||||
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0,
|
||||
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8,
|
||||
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0,
|
||||
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79,
|
||||
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80,
|
||||
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF,
|
||||
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0,
|
||||
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1,
|
||||
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E,
|
||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F,
|
||||
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF,
|
||||
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38,
|
||||
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2,
|
||||
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF,
|
||||
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
||||
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0,
|
||||
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98,
|
||||
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE,
|
||||
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns fr_patterns = {
|
||||
fr_trie_data,
|
||||
sizeof(fr_trie_data),
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,10 @@
|
||||
#include <SDCardManager.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include "../../Epub.h"
|
||||
#include "../Page.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
#include "../converters/ImageToFramebufferDecoder.h"
|
||||
|
||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
@ -16,13 +19,20 @@ constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
|
||||
const char* LIST_TAGS[] = {"ol", "ul"};
|
||||
constexpr int NUM_LIST_TAGS = sizeof(LIST_TAGS) / sizeof(LIST_TAGS[0]);
|
||||
|
||||
const char* BOLD_TAGS[] = {"b", "strong"};
|
||||
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
||||
|
||||
const char* ITALIC_TAGS[] = {"i", "em"};
|
||||
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
||||
|
||||
const char* IMAGE_TAGS[] = {"img"};
|
||||
const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||
constexpr int NUM_UNDERLINE_TAGS = sizeof(UNDERLINE_TAGS) / sizeof(UNDERLINE_TAGS[0]);
|
||||
|
||||
// Include "image" for SVG <image> elements (common in Calibre-generated covers)
|
||||
const char* IMAGE_TAGS[] = {"img", "image"};
|
||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
|
||||
const char* SKIP_TAGS[] = {"head"};
|
||||
@ -40,37 +50,77 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
||||
return false;
|
||||
}
|
||||
|
||||
// flush the contents of partWordBuffer to currentTextBlock
|
||||
// Create a BlockStyle from CSS style properties
|
||||
BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle) {
|
||||
BlockStyle blockStyle;
|
||||
blockStyle.marginTop = static_cast<int8_t>(cssStyle.marginTop + cssStyle.paddingTop);
|
||||
blockStyle.marginBottom = static_cast<int8_t>(cssStyle.marginBottom + cssStyle.paddingBottom);
|
||||
blockStyle.paddingTop = cssStyle.paddingTop;
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
||||
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
||||
blockStyle.marginLeft = static_cast<int16_t>(cssStyle.marginLeft);
|
||||
return blockStyle;
|
||||
}
|
||||
|
||||
// Update effective bold/italic/underline based on block style and inline style stack
|
||||
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||
// Start with block-level styles
|
||||
effectiveBold = currentBlockStyle.hasFontWeight() && currentBlockStyle.fontWeight == CssFontWeight::Bold;
|
||||
effectiveItalic = currentBlockStyle.hasFontStyle() && currentBlockStyle.fontStyle == CssFontStyle::Italic;
|
||||
effectiveUnderline =
|
||||
currentBlockStyle.hasTextDecoration() && currentBlockStyle.decoration == CssTextDecoration::Underline;
|
||||
|
||||
// Apply inline style stack in order
|
||||
for (const auto& entry : inlineStyleStack) {
|
||||
if (entry.hasBold) {
|
||||
effectiveBold = entry.bold;
|
||||
}
|
||||
if (entry.hasItalic) {
|
||||
effectiveItalic = entry.italic;
|
||||
}
|
||||
if (entry.hasUnderline) {
|
||||
effectiveUnderline = entry.underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the contents of partWordBuffer to currentTextBlock
|
||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
// determine font style
|
||||
if (partWordBufferIndex == 0) return;
|
||||
|
||||
// Determine font style using effective styles
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
||||
if (effectiveBold && effectiveItalic) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (boldUntilDepth < depth) {
|
||||
} else if (effectiveBold) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (italicUntilDepth < depth) {
|
||||
} else if (effectiveItalic) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
// flush the buffer
|
||||
|
||||
// Flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, effectiveUnderline);
|
||||
partWordBufferIndex = 0;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style, const BlockStyle& blockStyle) {
|
||||
if (currentTextBlock) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
if (currentTextBlock->isEmpty()) {
|
||||
currentTextBlock->setStyle(style);
|
||||
currentTextBlock->setBlockStyle(blockStyle);
|
||||
return;
|
||||
}
|
||||
|
||||
makePages();
|
||||
}
|
||||
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled));
|
||||
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { startNewTextBlock(style, BlockStyle{}); }
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
@ -80,46 +130,177 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract class and style attributes for CSS processing
|
||||
std::string classAttr;
|
||||
std::string styleAttr;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "class") == 0) {
|
||||
classAttr = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "style") == 0) {
|
||||
styleAttr = atts[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for tables - show placeholder text instead of dropping silently
|
||||
if (strcmp(name, "table") == 0) {
|
||||
// Add placeholder text
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
if (self->currentTextBlock) {
|
||||
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
|
||||
}
|
||||
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for a element with text)
|
||||
// Skip table contents
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
std::string alt = "[Image]";
|
||||
std::string src;
|
||||
std::string alt;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "alt") == 0) {
|
||||
if (strlen(atts[i + 1]) > 0) {
|
||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
||||
}
|
||||
break;
|
||||
// Standard HTML img uses "src", SVG image uses "xlink:href" or "href"
|
||||
if (strcmp(atts[i], "src") == 0 || strcmp(atts[i], "xlink:href") == 0 || strcmp(atts[i], "href") == 0) {
|
||||
src = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "alt") == 0) {
|
||||
alt = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.empty()) {
|
||||
Serial.printf("[%lu] [EHP] Found image: src=%s\n", millis(), src.c_str());
|
||||
|
||||
// Get the spine item's href to resolve the relative path
|
||||
size_t lastUnderscore = self->filepath.rfind('_');
|
||||
if (lastUnderscore != std::string::npos && lastUnderscore > 0) {
|
||||
std::string indexStr = self->filepath.substr(lastUnderscore + 1);
|
||||
indexStr.resize(indexStr.find('.'));
|
||||
int spineIndex = atoi(indexStr.c_str());
|
||||
|
||||
const auto& spineItem = self->epub->getSpineItem(spineIndex);
|
||||
std::string htmlHref = spineItem.href;
|
||||
size_t lastSlash = htmlHref.find_last_of('/');
|
||||
std::string htmlDir = (lastSlash != std::string::npos) ? htmlHref.substr(0, lastSlash + 1) : "";
|
||||
|
||||
// Resolve the image path relative to the HTML file
|
||||
std::string imageHref = src;
|
||||
while (imageHref.find("../") == 0) {
|
||||
imageHref = imageHref.substr(3);
|
||||
if (!htmlDir.empty()) {
|
||||
size_t dirSlash = htmlDir.find_last_of('/', htmlDir.length() - 2);
|
||||
htmlDir = (dirSlash != std::string::npos) ? htmlDir.substr(0, dirSlash + 1) : "";
|
||||
}
|
||||
}
|
||||
std::string resolvedPath = htmlDir + imageHref;
|
||||
|
||||
// Create a unique filename for the cached image
|
||||
std::string ext;
|
||||
size_t extPos = resolvedPath.rfind('.');
|
||||
if (extPos != std::string::npos) {
|
||||
ext = resolvedPath.substr(extPos);
|
||||
}
|
||||
std::string cachedImagePath = self->epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_" +
|
||||
std::to_string(self->imageCounter++) + ext;
|
||||
|
||||
// Extract image to cache file
|
||||
FsFile cachedImageFile;
|
||||
bool extractSuccess = false;
|
||||
if (SdMan.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) {
|
||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||
cachedImageFile.flush();
|
||||
cachedImageFile.close();
|
||||
delay(50); // Give SD card time to sync
|
||||
}
|
||||
|
||||
if (extractSuccess) {
|
||||
// Get image dimensions
|
||||
ImageDimensions dims = {0, 0};
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
||||
Serial.printf("[%lu] [EHP] Image dimensions: %dx%d\n", millis(), dims.width, dims.height);
|
||||
|
||||
// Scale to fit viewport while maintaining aspect ratio
|
||||
int maxWidth = self->viewportWidth;
|
||||
int maxHeight = self->viewportHeight;
|
||||
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
||||
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
|
||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (scale > 1.0f) scale = 1.0f;
|
||||
|
||||
int displayWidth = (int)(dims.width * scale);
|
||||
int displayHeight = (int)(dims.height * scale);
|
||||
|
||||
Serial.printf("[%lu] [EHP] Display size: %dx%d (scale %.2f)\n", millis(), displayWidth, displayHeight,
|
||||
scale);
|
||||
|
||||
// Create page for image
|
||||
if (self->currentPage && !self->currentPage->elements.empty()) {
|
||||
self->completePageFn(std::move(self->currentPage));
|
||||
self->currentPage.reset(new Page());
|
||||
if (!self->currentPage) {
|
||||
Serial.printf("[%lu] [EHP] Failed to create new page\n", millis());
|
||||
return;
|
||||
}
|
||||
self->currentPageNextY = 0;
|
||||
} else if (!self->currentPage) {
|
||||
self->currentPage.reset(new Page());
|
||||
if (!self->currentPage) {
|
||||
Serial.printf("[%lu] [EHP] Failed to create initial page\n", millis());
|
||||
return;
|
||||
}
|
||||
self->currentPageNextY = 0;
|
||||
}
|
||||
|
||||
// Create ImageBlock and add to page
|
||||
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight);
|
||||
if (!imageBlock) {
|
||||
Serial.printf("[%lu] [EHP] Failed to create ImageBlock\n", millis());
|
||||
return;
|
||||
}
|
||||
int xPos = (self->viewportWidth - displayWidth) / 2;
|
||||
auto pageImage = std::make_shared<PageImage>(imageBlock, xPos, self->currentPageNextY);
|
||||
if (!pageImage) {
|
||||
Serial.printf("[%lu] [EHP] Failed to create PageImage\n", millis());
|
||||
return;
|
||||
}
|
||||
self->currentPage->elements.push_back(pageImage);
|
||||
self->currentPageNextY += displayHeight;
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
} else {
|
||||
Serial.printf("[%lu] [EHP] Failed to get image dimensions\n", millis());
|
||||
SdMan.remove(cachedImagePath.c_str());
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[%lu] [EHP] Failed to extract image\n", millis());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: show placeholder text when image processing fails
|
||||
// This handles progressive JPEGs, unsupported formats, memory issues, etc.
|
||||
std::string placeholder;
|
||||
if (!alt.empty()) {
|
||||
placeholder = "[Image: " + alt + "]";
|
||||
} else if (!src.empty()) {
|
||||
// Extract filename from path for a more informative placeholder
|
||||
size_t lastSlash = src.find_last_of('/');
|
||||
std::string filename = (lastSlash != std::string::npos) ? src.substr(lastSlash + 1) : src;
|
||||
placeholder = "[Image: " + filename + "]";
|
||||
} else {
|
||||
placeholder = "[Image unavailable]";
|
||||
}
|
||||
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
self->characterData(userData, placeholder.c_str(), placeholder.length());
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for a element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||
@ -141,46 +322,254 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
|
||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
// Determine if this is a block element
|
||||
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||
bool isListTag = matches(name, LIST_TAGS, NUM_LIST_TAGS);
|
||||
|
||||
// Handle list container tags (ol, ul)
|
||||
if (isListTag) {
|
||||
ListContext ctx;
|
||||
ctx.isOrdered = strcmp(name, "ol") == 0;
|
||||
ctx.counter = 0;
|
||||
ctx.depth = self->depth;
|
||||
self->listStack.push_back(ctx);
|
||||
self->depth += 1;
|
||||
return;
|
||||
return; // Lists themselves don't create text blocks
|
||||
}
|
||||
|
||||
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
if (strcmp(name, "br") == 0) {
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||
self->flushPartWordBuffer();
|
||||
// Compute CSS style for this element
|
||||
CssStyle cssStyle;
|
||||
if (self->cssParser) {
|
||||
// Get combined tag + class styles
|
||||
cssStyle = self->cssParser->resolveStyle(name, classAttr);
|
||||
// Merge inline style (highest priority)
|
||||
if (!styleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||
cssStyle.merge(inlineStyle);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
// Headers: center aligned, bold, apply CSS overrides
|
||||
TextBlock::Style alignment = TextBlock::CENTER_ALIGN;
|
||||
if (cssStyle.hasTextAlign()) {
|
||||
switch (cssStyle.alignment) {
|
||||
case TextAlign::Left:
|
||||
alignment = TextBlock::LEFT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Right:
|
||||
alignment = TextBlock::RIGHT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Center:
|
||||
alignment = TextBlock::CENTER_ALIGN;
|
||||
break;
|
||||
case TextAlign::Justify:
|
||||
alignment = TextBlock::JUSTIFIED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self->currentBlockStyle = cssStyle;
|
||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle));
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
if (strcmp(name, "br") == 0) {
|
||||
// Flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||
// This fixes issue where <br/> incorrectly wrapped the preceding word to a new line
|
||||
self->flushPartWordBuffer();
|
||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||
} else if (strcmp(name, "li") == 0) {
|
||||
// For list items, DON'T create a text block yet - wait for the first content element
|
||||
// This prevents the marker from being on its own line when <li><p>content</p></li>
|
||||
self->insideListItem = true;
|
||||
self->listItemDepth = self->depth;
|
||||
self->listItemHasContent = false;
|
||||
|
||||
// Increment counter now (so nested lists work correctly)
|
||||
if (!self->listStack.empty()) {
|
||||
self->listStack.back().counter++;
|
||||
}
|
||||
// Don't create text block or add marker yet - will be done when first content arrives
|
||||
self->depth += 1;
|
||||
return;
|
||||
} else {
|
||||
// Determine alignment from CSS or default
|
||||
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
||||
if (cssStyle.hasTextAlign()) {
|
||||
switch (cssStyle.alignment) {
|
||||
case TextAlign::Left:
|
||||
alignment = TextBlock::LEFT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Right:
|
||||
alignment = TextBlock::RIGHT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Center:
|
||||
alignment = TextBlock::CENTER_ALIGN;
|
||||
break;
|
||||
case TextAlign::Justify:
|
||||
alignment = TextBlock::JUSTIFIED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply default styling for blockquote if no CSS margin is specified
|
||||
const bool isBlockquote = strcmp(name, "blockquote") == 0;
|
||||
if (isBlockquote) {
|
||||
if (!cssStyle.hasMarginLeft()) {
|
||||
// Default left indent for blockquotes (~1.5em at 16px base = 24px)
|
||||
cssStyle.marginLeft = 24.0f;
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
}
|
||||
// Also make blockquotes italic by default if not specified
|
||||
if (!cssStyle.hasFontStyle()) {
|
||||
cssStyle.fontStyle = CssFontStyle::Italic;
|
||||
cssStyle.defined.fontStyle = 1;
|
||||
}
|
||||
// Track blockquote context for child elements
|
||||
self->insideBlockquote = true;
|
||||
self->blockquoteDepth = self->depth;
|
||||
self->blockquoteMarginLeft = cssStyle.marginLeft;
|
||||
}
|
||||
|
||||
// Apply blockquote styling to child block elements
|
||||
if (self->insideBlockquote && !isBlockquote) {
|
||||
// Inherit margin and border from parent blockquote
|
||||
if (!cssStyle.hasMarginLeft()) {
|
||||
cssStyle.marginLeft = self->blockquoteMarginLeft;
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply left margin to list items (indent the whole block)
|
||||
if (self->insideListItem && !cssStyle.hasMarginLeft()) {
|
||||
// Default left indent for list items (~1.5em at 16px base = 24px)
|
||||
cssStyle.marginLeft = 24.0f;
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
}
|
||||
|
||||
self->currentBlockStyle = cssStyle;
|
||||
BlockStyle blockStyleForElement = createBlockStyleFromCss(cssStyle);
|
||||
if (isBlockquote || self->insideBlockquote) {
|
||||
blockStyleForElement.hasLeftBorder = true; // Draw vertical bar for blockquotes
|
||||
}
|
||||
self->startNewTextBlock(alignment, blockStyleForElement);
|
||||
self->updateEffectiveInlineStyle();
|
||||
|
||||
// If this is a blockquote, apply italic styling
|
||||
if (isBlockquote && cssStyle.hasFontStyle() && cssStyle.fontStyle == CssFontStyle::Italic) {
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
}
|
||||
|
||||
// If this is the first block element inside a list item, add the marker
|
||||
if (self->insideListItem && !self->listItemHasContent) {
|
||||
if (!self->listStack.empty()) {
|
||||
const ListContext& ctx = self->listStack.back();
|
||||
if (ctx.isOrdered) {
|
||||
// Ordered list: use number (counter was already incremented)
|
||||
std::string marker = std::to_string(ctx.counter) + ". ";
|
||||
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
|
||||
} else {
|
||||
// Unordered list: use bullet
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
} else {
|
||||
// No list context (orphan li), use bullet as fallback
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
self->listItemHasContent = true;
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment));
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||
// Push inline style entry for underline tag
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = true;
|
||||
if (cssStyle.hasFontWeight()) {
|
||||
entry.hasBold = true;
|
||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||
}
|
||||
if (cssStyle.hasFontStyle()) {
|
||||
entry.hasItalic = true;
|
||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
// Push inline style entry for bold tag
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
entry.hasBold = true;
|
||||
entry.bold = true;
|
||||
if (cssStyle.hasFontStyle()) {
|
||||
entry.hasItalic = true;
|
||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.decoration == CssTextDecoration::Underline;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
return;
|
||||
// Push inline style entry for italic tag
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
entry.hasItalic = true;
|
||||
entry.italic = true;
|
||||
if (cssStyle.hasFontWeight()) {
|
||||
entry.hasBold = true;
|
||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.decoration == CssTextDecoration::Underline;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (strcmp(name, "span") == 0 || !isBlockElement) {
|
||||
// Handle span and other inline elements for CSS styling
|
||||
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
// This prevents text accumulated before this element from getting the new style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
if (cssStyle.hasFontWeight()) {
|
||||
entry.hasBold = true;
|
||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||
}
|
||||
if (cssStyle.hasFontStyle()) {
|
||||
entry.hasItalic = true;
|
||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.decoration == CssTextDecoration::Underline;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
}
|
||||
|
||||
// Unprocessed tag, just increasing depth and continue forward
|
||||
self->depth += 1;
|
||||
}
|
||||
|
||||
@ -192,11 +581,59 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture byte offset of this character data for page position tracking
|
||||
if (self->xmlParser) {
|
||||
self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser);
|
||||
}
|
||||
|
||||
// If we're inside an <li> but no text block was created yet (direct text without inner <p>),
|
||||
// create a text block and add the list marker now
|
||||
if (self->insideListItem && !self->listItemHasContent) {
|
||||
// Apply left margin for list items
|
||||
CssStyle cssStyle;
|
||||
cssStyle.marginLeft = 24.0f; // Default indent (~1.5em at 16px base)
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
|
||||
BlockStyle blockStyle = createBlockStyleFromCss(cssStyle);
|
||||
self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment), blockStyle);
|
||||
|
||||
// Add the list marker
|
||||
if (!self->listStack.empty()) {
|
||||
const ListContext& ctx = self->listStack.back();
|
||||
if (ctx.isOrdered) {
|
||||
std::string marker = std::to_string(ctx.counter) + ". ";
|
||||
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
|
||||
} else {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
} else {
|
||||
// No list context (orphan li), use bullet as fallback
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
self->listItemHasContent = true;
|
||||
}
|
||||
|
||||
// Determine font style from depth-based tracking and CSS effective style
|
||||
const bool isBold = self->boldUntilDepth < self->depth || self->effectiveBold;
|
||||
const bool isItalic = self->italicUntilDepth < self->depth || self->effectiveItalic;
|
||||
const bool isUnderline = self->underlineUntilDepth < self->depth || self->effectiveUnderline;
|
||||
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (isBold && isItalic) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (isBold) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (isItalic) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (isWhitespace(s[i])) {
|
||||
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
self->flushPartWordBuffer();
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle, isUnderline);
|
||||
self->partWordBufferIndex = 0;
|
||||
}
|
||||
// Skip the whitespace char
|
||||
continue;
|
||||
@ -218,7 +655,9 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
|
||||
// If we're about to run out of space, then cut the word off and start a new one
|
||||
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||
self->flushPartWordBuffer();
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle, isUnderline);
|
||||
self->partWordBufferIndex = 0;
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
@ -239,18 +678,44 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
||||
// We don't want to flush out content when closing inline tags like <span>.
|
||||
// Currently this also flushes out on closing <b> and <i> tags, but they are line tags so that shouldn't happen,
|
||||
// text styling needs to be overhauled to fix it.
|
||||
const bool shouldBreakText =
|
||||
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
||||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
strcmp(name, "table") == 0 || matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||
// Check if any style state will change after we decrement depth
|
||||
// If so, we MUST flush the partWordBuffer with the CURRENT style first
|
||||
// Note: depth hasn't been decremented yet, so we check against (depth - 1)
|
||||
const bool willPopStyleStack =
|
||||
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
||||
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
||||
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
||||
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
||||
|
||||
if (shouldBreakText) {
|
||||
self->flushPartWordBuffer();
|
||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||
|
||||
// Flush buffer with current style BEFORE any style changes
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// Flush if style will change OR if we're closing a block/structural element
|
||||
const bool shouldFlush = styleWillChange || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) ||
|
||||
matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) ||
|
||||
matches(name, LIST_TAGS, NUM_LIST_TAGS) || self->depth == 1;
|
||||
|
||||
if (shouldFlush) {
|
||||
// Use combined depth-based and CSS-based style
|
||||
const bool isBold = self->boldUntilDepth < self->depth || self->effectiveBold;
|
||||
const bool isItalic = self->italicUntilDepth < self->depth || self->effectiveItalic;
|
||||
const bool isUnderline = self->underlineUntilDepth < self->depth || self->effectiveUnderline;
|
||||
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (isBold && isItalic) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (isBold) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (isItalic) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle, isUnderline);
|
||||
self->partWordBufferIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,31 +726,71 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
self->skipUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving bold
|
||||
// Leaving list container (ol, ul)
|
||||
if (matches(name, LIST_TAGS, NUM_LIST_TAGS)) {
|
||||
if (!self->listStack.empty() && self->listStack.back().depth == self->depth) {
|
||||
self->listStack.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Leaving list item (li)
|
||||
if (strcmp(name, "li") == 0 && self->listItemDepth == self->depth) {
|
||||
self->insideListItem = false;
|
||||
self->listItemDepth = INT_MAX;
|
||||
self->listItemHasContent = false;
|
||||
}
|
||||
|
||||
// Leaving blockquote
|
||||
if (strcmp(name, "blockquote") == 0 && self->blockquoteDepth == self->depth) {
|
||||
self->insideBlockquote = false;
|
||||
self->blockquoteDepth = INT_MAX;
|
||||
self->blockquoteMarginLeft = 0.0f;
|
||||
}
|
||||
|
||||
// Leaving bold tag
|
||||
if (self->boldUntilDepth == self->depth) {
|
||||
self->boldUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving italic
|
||||
// Leaving italic tag
|
||||
if (self->italicUntilDepth == self->depth) {
|
||||
self->italicUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving underline tag
|
||||
if (self->underlineUntilDepth == self->depth) {
|
||||
self->underlineUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Pop from inline style stack if we pushed an entry at this depth
|
||||
// This handles all inline elements: b, i, u, span, etc.
|
||||
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
||||
self->inlineStyleStack.pop_back();
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
|
||||
// Clear block style when leaving block elements
|
||||
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
self->currentBlockStyle.reset();
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
}
|
||||
|
||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
||||
|
||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
xmlParser = XML_ParserCreate(nullptr);
|
||||
int done;
|
||||
|
||||
if (!parser) {
|
||||
if (!xmlParser) {
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("EHP", filepath, file)) {
|
||||
XML_ParserFree(parser);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -294,18 +799,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
size_t bytesRead = 0;
|
||||
int lastProgress = -1;
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
// Initialize offset tracking - first page starts at offset 0
|
||||
currentPageStartOffset = 0;
|
||||
lastCharDataOffset = 0;
|
||||
|
||||
XML_SetUserData(xmlParser, this);
|
||||
XML_SetElementHandler(xmlParser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(xmlParser, characterData);
|
||||
|
||||
do {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
void* const buf = XML_GetBuffer(xmlParser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@ -314,10 +824,11 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
|
||||
if (len == 0 && file.available() > 0) {
|
||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@ -335,27 +846,33 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
|
||||
done = file.available() == 0;
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
if (XML_ParseBuffer(xmlParser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(xmlParser),
|
||||
XML_ErrorString(XML_GetErrorCode(xmlParser)));
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
|
||||
// Process last page if there is still text
|
||||
if (currentTextBlock) {
|
||||
makePages();
|
||||
// Set the content offset for the final page
|
||||
if (currentPage) {
|
||||
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
|
||||
}
|
||||
completePageFn(std::move(currentPage));
|
||||
currentPage.reset();
|
||||
currentTextBlock.reset();
|
||||
@ -368,8 +885,15 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
|
||||
if (currentPageNextY + lineHeight > viewportHeight) {
|
||||
// Set the content offset for the page being completed
|
||||
if (currentPage) {
|
||||
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
|
||||
}
|
||||
completePageFn(std::move(currentPage));
|
||||
|
||||
// Start new page - offset will be set when first content is added
|
||||
currentPage.reset(new Page());
|
||||
currentPageStartOffset = lastCharDataOffset; // Use offset from when content was parsed
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
@ -385,14 +909,29 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
// Use offset captured during character data parsing
|
||||
currentPageStartOffset = lastCharDataOffset;
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
|
||||
// Apply marginTop before the paragraph
|
||||
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
||||
if (blockStyle.marginTop > 0) {
|
||||
currentPageNextY += lineHeight * blockStyle.marginTop;
|
||||
}
|
||||
|
||||
currentTextBlock->layoutAndExtractLines(
|
||||
renderer, fontId, viewportWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
||||
// Extra paragraph spacing if enabled
|
||||
|
||||
// Apply marginBottom after the paragraph
|
||||
if (blockStyle.marginBottom > 0) {
|
||||
currentPageNextY += lineHeight * blockStyle.marginBottom;
|
||||
}
|
||||
|
||||
// Extra paragraph spacing if enabled (default behavior)
|
||||
if (extraParagraphSpacing) {
|
||||
currentPageNextY += lineHeight / 2;
|
||||
}
|
||||
|
||||
@ -7,14 +7,19 @@
|
||||
#include <memory>
|
||||
|
||||
#include "../ParsedText.h"
|
||||
#include "../blocks/ImageBlock.h"
|
||||
#include "../blocks/TextBlock.h"
|
||||
#include "../css/CssParser.h"
|
||||
#include "../css/CssStyle.h"
|
||||
|
||||
class Page;
|
||||
class GfxRenderer;
|
||||
class Epub;
|
||||
|
||||
#define MAX_WORD_SIZE 200
|
||||
|
||||
class ChapterHtmlSlimParser {
|
||||
std::shared_ptr<Epub> epub;
|
||||
const std::string& filepath;
|
||||
GfxRenderer& renderer;
|
||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||
@ -23,6 +28,7 @@ class ChapterHtmlSlimParser {
|
||||
int skipUntilDepth = INT_MAX;
|
||||
int boldUntilDepth = INT_MAX;
|
||||
int italicUntilDepth = INT_MAX;
|
||||
int underlineUntilDepth = INT_MAX;
|
||||
// buffer for building up words from characters, will auto break if longer than this
|
||||
// leave one char at end for null pointer
|
||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||
@ -37,8 +43,46 @@ class ChapterHtmlSlimParser {
|
||||
uint16_t viewportWidth;
|
||||
uint16_t viewportHeight;
|
||||
bool hyphenationEnabled;
|
||||
const CssParser* cssParser;
|
||||
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 hasUnderline = false, underline = false;
|
||||
};
|
||||
std::vector<StyleStackEntry> inlineStyleStack;
|
||||
CssStyle currentBlockStyle;
|
||||
bool effectiveBold = false;
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
|
||||
// List context tracking for ordered/unordered lists
|
||||
struct ListContext {
|
||||
bool isOrdered = false; // true for <ol>, false for <ul>
|
||||
int counter = 0; // Current item number (for ordered lists)
|
||||
int depth = 0; // Depth at which list was opened
|
||||
};
|
||||
std::vector<ListContext> listStack;
|
||||
bool insideListItem = false; // True when we're inside an <li> element
|
||||
int listItemDepth = INT_MAX; // Depth at which <li> was opened
|
||||
bool listItemHasContent = false; // True if we've added content to the current list item
|
||||
|
||||
// Blockquote context tracking (for left border on child elements)
|
||||
bool insideBlockquote = false;
|
||||
int blockquoteDepth = INT_MAX;
|
||||
float blockquoteMarginLeft = 0.0f; // Inherit margin from blockquote to child elements
|
||||
|
||||
// Byte offset tracking for position restoration after re-indexing
|
||||
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
||||
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
||||
size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing)
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(TextBlock::Style style);
|
||||
void startNewTextBlock(TextBlock::Style style, const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
// XML callbacks
|
||||
@ -47,13 +91,15 @@ class ChapterHtmlSlimParser {
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
||||
const float lineCompression, const bool extraParagraphSpacing,
|
||||
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
|
||||
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||
const std::function<void(int)>& progressFn = nullptr)
|
||||
: filepath(filepath),
|
||||
const std::function<void(int)>& progressFn = nullptr,
|
||||
const CssParser* cssParser = nullptr)
|
||||
: epub(epub),
|
||||
filepath(filepath),
|
||||
renderer(renderer),
|
||||
fontId(fontId),
|
||||
lineCompression(lineCompression),
|
||||
@ -63,7 +109,8 @@ class ChapterHtmlSlimParser {
|
||||
viewportHeight(viewportHeight),
|
||||
hyphenationEnabled(hyphenationEnabled),
|
||||
completePageFn(completePageFn),
|
||||
progressFn(progressFn) {}
|
||||
progressFn(progressFn),
|
||||
cssParser(cssParser) {}
|
||||
~ChapterHtmlSlimParser() = default;
|
||||
bool parseAndBuildPages();
|
||||
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
namespace {
|
||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||
constexpr char itemCacheFile[] = "/.items.bin";
|
||||
} // namespace
|
||||
|
||||
@ -218,6 +219,11 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
}
|
||||
}
|
||||
|
||||
// Collect CSS files
|
||||
if (mediaType == MEDIA_TYPE_CSS) {
|
||||
self->cssFiles.push_back(href);
|
||||
}
|
||||
|
||||
// EPUB 3: Check for nav document (properties contains "nav")
|
||||
if (!properties.empty() && self->tocNavPath.empty()) {
|
||||
// Properties is space-separated, check if "nav" is present as a word
|
||||
|
||||
@ -64,6 +64,7 @@ class ContentOpfParser final : public Print {
|
||||
std::string tocNavPath; // EPUB 3 nav document path
|
||||
std::string coverItemHref;
|
||||
std::string textReferenceHref;
|
||||
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
||||
|
||||
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||
BookMetadataCache* cache)
|
||||
|
||||
@ -82,6 +82,10 @@ const char* Bitmap::errorToString(BmpReaderError err) {
|
||||
|
||||
BmpReaderError Bitmap::parseHeaders() {
|
||||
if (!file) return BmpReaderError::FileInvalid;
|
||||
|
||||
// Store file size for cache validation
|
||||
fileSize = file.size();
|
||||
|
||||
if (!file.seek(0)) return BmpReaderError::SeekStartFailed;
|
||||
|
||||
// --- BMP FILE HEADER ---
|
||||
@ -262,3 +266,108 @@ BmpReaderError Bitmap::rewindToData() const {
|
||||
|
||||
return BmpReaderError::Ok;
|
||||
}
|
||||
|
||||
EdgeLuminance Bitmap::detectEdgeLuminance(int depth) const {
|
||||
// Detect average luminance for each edge of the image.
|
||||
// Samples 'depth' pixels from each edge for more stable averages.
|
||||
// Returns per-edge luminance values (0-255).
|
||||
|
||||
EdgeLuminance result = {128, 128, 128, 128}; // Default to neutral gray
|
||||
|
||||
if (width <= 0 || height <= 0) return result;
|
||||
if (depth < 1) depth = 1;
|
||||
if (depth > width / 2) depth = width / 2;
|
||||
if (depth > height / 2) depth = height / 2;
|
||||
|
||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(rowBytes));
|
||||
if (!rowBuffer) return result;
|
||||
|
||||
// Accumulators for each edge
|
||||
uint32_t topSum = 0, bottomSum = 0, leftSum = 0, rightSum = 0;
|
||||
int topCount = 0, bottomCount = 0, leftCount = 0, rightCount = 0;
|
||||
|
||||
// Helper lambda to get luminance from a pixel at position x in rowBuffer
|
||||
auto getLuminance = [&](int x) -> uint8_t {
|
||||
switch (bpp) {
|
||||
case 32: {
|
||||
const uint8_t* p = rowBuffer + x * 4;
|
||||
return (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||
}
|
||||
case 24: {
|
||||
const uint8_t* p = rowBuffer + x * 3;
|
||||
return (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||
}
|
||||
case 8:
|
||||
return paletteLum[rowBuffer[x]];
|
||||
case 2:
|
||||
return paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
|
||||
case 1: {
|
||||
const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0;
|
||||
return paletteLum[palIndex];
|
||||
}
|
||||
default:
|
||||
return 128; // Neutral if unsupported
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to seek to a specific image row (accounting for top-down vs bottom-up)
|
||||
auto seekToRow = [&](int imageRow) -> bool {
|
||||
// In bottom-up BMP (topDown=false), row 0 in file is the bottom row of image
|
||||
// In top-down BMP (topDown=true), row 0 in file is the top row of image
|
||||
int fileRow = topDown ? imageRow : (height - 1 - imageRow);
|
||||
return file.seek(bfOffBits + static_cast<uint32_t>(fileRow) * rowBytes);
|
||||
};
|
||||
|
||||
// Sample top rows (image rows 0 to depth-1) - all pixels
|
||||
for (int row = 0; row < depth && row < height; row++) {
|
||||
if (seekToRow(row) && file.read(rowBuffer, rowBytes) == rowBytes) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
topSum += getLuminance(x);
|
||||
topCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sample bottom rows (image rows height-depth to height-1) - all pixels
|
||||
for (int row = height - depth; row < height; row++) {
|
||||
if (row >= depth && row >= 0) { // Avoid overlap with top rows
|
||||
if (seekToRow(row) && file.read(rowBuffer, rowBytes) == rowBytes) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
bottomSum += getLuminance(x);
|
||||
bottomCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sample left and right edges from all rows
|
||||
for (int y = 0; y < height; y++) {
|
||||
if (seekToRow(y) && file.read(rowBuffer, rowBytes) == rowBytes) {
|
||||
// Left edge (first 'depth' pixels)
|
||||
for (int x = 0; x < depth && x < width; x++) {
|
||||
leftSum += getLuminance(x);
|
||||
leftCount++;
|
||||
}
|
||||
// Right edge (last 'depth' pixels)
|
||||
for (int x = width - depth; x < width; x++) {
|
||||
if (x >= depth) { // Avoid overlap with left edge
|
||||
rightSum += getLuminance(x);
|
||||
rightCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
|
||||
// Calculate averages
|
||||
if (topCount > 0) result.top = static_cast<uint8_t>(topSum / topCount);
|
||||
if (bottomCount > 0) result.bottom = static_cast<uint8_t>(bottomSum / bottomCount);
|
||||
if (leftCount > 0) result.left = static_cast<uint8_t>(leftSum / leftCount);
|
||||
if (rightCount > 0) result.right = static_cast<uint8_t>(rightSum / rightCount);
|
||||
|
||||
// Rewind file position for subsequent drawing
|
||||
rewindToData();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user